Skip to content

Commit

Permalink
feat(yt): mark video as watched
Browse files Browse the repository at this point in the history
works flawlessly with connection issues & accounts switching
  • Loading branch information
MSOB7YY committed Jul 15, 2024
1 parent 0ebbcd6 commit 2d12b32
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 11 deletions.
38 changes: 38 additions & 0 deletions lib/base/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,12 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
}
});

// -- hmm marking local tracks as yt-watched..?
// final trackYoutubeId = tr.youtubeID;
// if (trackYoutubeId.isNotEmpty) {
// YoutubeInfoController.history.markVideoWatched(videoId: trackYoutubeId, streamResult: null, errorOnMissingParam: false);
// }

Duration? duration;

Future<Duration?> setPls() async {
Expand Down Expand Up @@ -850,6 +856,13 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
VideoStreamInfo? _ytNotificationVideoInfo;
File? _ytNotificationVideoThumbnail;

/// Shows error if [marked] is not true.
void _onVideoMarkWatchResultError(bool? marked) {
if (marked != true) {
snackyy(message: 'Failed to mark video as watched.', top: false, isError: true);
}
}

Future<void> onItemPlayYoutubeID(
Q pi,
YoutubeID item,
Expand Down Expand Up @@ -1023,6 +1036,16 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

if (checkInterrupted()) return;

Completer<bool?>? markedAsWatched;

// only if was playing
if (okaySetFromCache() && (streamsResult != null || !ConnectivityController.inst.hasConnection)) {
// -- allow when no connection bcz this function won't try again with no connection,
// -- so we force call here and let `markVideoWatched` do the job when there is proper connection.
markedAsWatched = Completer<bool?>();
markedAsWatched.complete(YoutubeInfoController.history.markVideoWatched(videoId: item.id, streamResult: streamsResult));
}

if (ConnectivityController.inst.hasConnection) {
try {
isFetchingInfo.value = true;
Expand All @@ -1040,6 +1063,21 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
snackyy(message: 'Error getting streams', top: false, isError: true);
return null;
});
if (streamsResult != null) {
if (markedAsWatched != null) {
// -- older request was initiated, wait to see the value.
markedAsWatched.future.then(
(marked) {
if (marked != true && streamsResult != null) {
YoutubeInfoController.history.markVideoWatched(videoId: item.id, streamResult: streamsResult).then(_onVideoMarkWatchResultError);
}
},
);
} else {
// -- no old requests, force mark
YoutubeInfoController.history.markVideoWatched(videoId: item.id, streamResult: streamsResult).then(_onVideoMarkWatchResultError);
}
}
duration ??= streamsResult?.audioStreams.firstOrNull?.duration;
onInfoOrThumbObtained(info: streamsResult?.info);
if (checkInterrupted(refreshNoti: false)) return; // -- onInfoOrThumbObtained refreshes notification.
Expand Down
15 changes: 15 additions & 0 deletions lib/controller/connectivity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import 'dart:async';

import 'package:connectivity_plus/connectivity_plus.dart';

import 'package:namida/core/extensions.dart';
import 'package:namida/core/utils.dart';

class ConnectivityController {
Expand All @@ -24,10 +26,23 @@ class ConnectivityController {
connections.contains(ConnectivityResult.other);
_hasHighConnection.value = highConnection;
_hasConnection.value = true;
if (_onConnectionRestored.isNotEmpty) {
_onConnectionRestored.loop((item) => item());
}
}
});
}

final _onConnectionRestored = <void Function()>[];

void registerOnConnectionRestored(void Function() fn) {
_onConnectionRestored.add(fn);
}

void removeOnConnectionRestored(void Function() fn) {
_onConnectionRestored.remove(fn);
}

bool get hasConnection => _hasConnection.value;
bool get hasHighConnection => _hasHighConnection.value;

Expand Down
185 changes: 185 additions & 0 deletions lib/youtube/controller/info_controllers/yt_history_linker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
part of '../youtube_info_controller.dart';

class _YoutubeHistoryLinker {
final String? Function() activeAccId;
_YoutubeHistoryLinker(this.activeAccId);

late String _dbDirectory;
void init(String directory) {
_dbDirectory = directory;
_ensureDBOpened();
ConnectivityController.inst.registerOnConnectionRestored(_onConnectionRestored);
}

void _onConnectionRestored() {
if (_hasPendingRequests) executePendingRequests();
}

String? _dbOpenedAccId;
void _ensureDBOpened() {
final accId = activeAccId();
if (accId == _dbOpenedAccId) return; // if both null, means no db will be opened, meaning db operations will not execute.. keikaku dori

_dbOpenedAccId = accId;
_pendingRequestsDBIdle?.close();
_pendingRequestsDBIdle = DBWrapper.open(_dbDirectory, 'pending_history_$accId');
_pendingRequestsCompleter?.completeIfWasnt();
_pendingRequestsCompleter = null;
executePendingRequests();
}

DBWrapper? _pendingRequestsDBIdle;
DBWrapper? get _pendingRequestsDB {
_ensureDBOpened();
return _pendingRequestsDBIdle;
}

bool get _hasConnection => ConnectivityController.inst.hasConnection;

bool _hasPendingRequests = true;

Completer<void>? _pendingRequestsCompleter;

void _addPendingRequest({required String videoId, required VideoStreamsResult? streamResult}) {
_hasPendingRequests = true;
final db = _pendingRequestsDB;
if (db == null) return;

final vId = streamResult?.videoId ?? videoId;
final key = "${vId}_${DateTime.now().millisecondsSinceEpoch}";
final map = {
'videoId': vId,
'statsPlaybackUrl': streamResult?.statsPlaybackUrl,
'statsWatchtimeUrl': streamResult?.statsWatchtimeUrl,
};
db.putAsync(key, map);
}

List<String> getPendingRequestsSync() {
final list = <String>[];
_pendingRequestsDB?.loadEverythingKeyed(
(key, value) {
list.add(key);
},
);
return list;
}

void executePendingRequests() async {
if (!_hasConnection) return null;

if (_pendingRequestsCompleter != null) {
// -- already executing
return;
}

_pendingRequestsCompleter ??= Completer<void>();

final queue = Queue(parallel: 1);

bool hadError = false;

int itemsAddedToQueue = 0;

final db = _pendingRequestsDB;

db?.loadEverythingKeyed(
(key, value) {
if (hadError) return;
if (!_hasConnection) {
hadError = true;
return;
}

itemsAddedToQueue++;

queue.add(
() async {
if (hadError) return;
if (!_hasConnection) {
hadError = true;
return;
}

bool added = false;
try {
String? statsPlaybackUrl = value['statsPlaybackUrl'];
String? statsWatchtimeUrl = value['statsWatchtimeUrl'];
if (statsPlaybackUrl == null && _hasConnection) {
final videoId = value['videoId'] ?? key.substring(0, 11);
final streamsRes = await YoutubeInfoController.video.fetchVideoStreams(videoId, forceRequest: true);
statsPlaybackUrl = streamsRes?.statsPlaybackUrl;
statsWatchtimeUrl ??= streamsRes?.statsWatchtimeUrl;
}
if (statsPlaybackUrl != null) {
// -- we check beforehand to supress internal error
final res = await YoutiPie.history.addVideoToHistory(
statsPlaybackUrl: statsPlaybackUrl,
statsWatchtimeUrl: statsWatchtimeUrl,
);
added = res.$1;
}
} catch (_) {}
if (added || _hasConnection) {
// had connection but didnt mark. idc
db.deleteAsync(key);
} else {
hadError = true; // no connection, will not proceed anymore
}
},
);
},
);

if (itemsAddedToQueue > 0) await queue.onComplete;

if (!hadError) _hasPendingRequests = false;

_pendingRequestsCompleter?.completeIfWasnt();
_pendingRequestsCompleter = null;
}

Future<bool?> markVideoWatched({required String videoId, required VideoStreamsResult? streamResult, bool errorOnMissingParam = true}) async {
if (_hasPendingRequests) {
executePendingRequests();
}

if (_pendingRequestsCompleter != null) {
await _pendingRequestsCompleter!.future;
}

bool added = false;

if (_hasConnection && !_hasPendingRequests) {
String? statsPlaybackUrl = streamResult?.statsPlaybackUrl;
String? statsWatchtimeUrl = streamResult?.statsWatchtimeUrl;
if (statsPlaybackUrl == null) {
final streamsRes = await YoutubeInfoController.video.fetchVideoStreams(videoId, forceRequest: false);
statsPlaybackUrl = streamsRes?.statsPlaybackUrl;
statsWatchtimeUrl ??= streamsRes?.statsWatchtimeUrl;
}
if (statsPlaybackUrl != null || errorOnMissingParam) {
final res = await YoutiPie.history.addVideoToHistory(
statsPlaybackUrl: statsPlaybackUrl,
statsWatchtimeUrl: statsWatchtimeUrl,
);
added = res.$1;
}
}
if (added) {
return added;
} else {
_addPendingRequest(videoId: videoId, streamResult: streamResult);
return null;
}
}

Future<YoutiPieHistoryResult?> fetchHistory({ExecuteDetails? details}) {
return YoutiPie.history.fetchHistory(details: details);
}

YoutiPieHistoryResult? fetchHistorySync() {
final cache = YoutiPie.cacheBuilder.forHistoryVideos();
return cache.read();
}
}
29 changes: 18 additions & 11 deletions lib/youtube/controller/youtube_info_controller.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
library namidayoutubeinfo;

import 'dart:async';
import 'dart:io';

import 'package:logger/logger.dart';
import 'package:namida/class/video.dart';
import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/youtube/controller/yt_miniplayer_ui_controller.dart';

import 'package:namico_db_wrapper/namico_db_wrapper.dart';
import 'package:queue/queue.dart';
import 'package:youtipie/class/channels/channel_page_about.dart';
import 'package:youtipie/class/channels/channel_page_result.dart';
import 'package:youtipie/class/channels/channel_tab.dart';
import 'package:youtipie/class/channels/tabs/channel_tab_videos_result.dart';
import 'package:youtipie/class/execute_details.dart';
import 'package:youtipie/class/related_videos_request_params.dart';
import 'package:youtipie/class/result_wrapper/comment_result.dart';
import 'package:youtipie/class/result_wrapper/history_result.dart';
import 'package:youtipie/class/result_wrapper/related_videos_result.dart';
import 'package:youtipie/class/result_wrapper/search_result.dart';
import 'package:youtipie/class/stream_info_item/stream_info_item.dart';
Expand All @@ -30,7 +23,19 @@ import 'package:youtipie/core/enum.dart';
import 'package:youtipie/core/http.dart';
import 'package:youtipie/youtipie.dart' hide logger;

import 'package:namida/class/video.dart';
import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/youtube/controller/yt_miniplayer_ui_controller.dart';

part 'info_controllers/yt_channel_info_controller.dart';
part 'info_controllers/yt_history_linker.dart';
part 'info_controllers/yt_search_info_controller.dart';
part 'info_controllers/yt_various_utils.dart';
part 'info_controllers/yt_video_info_controller.dart';
Expand All @@ -41,6 +46,7 @@ class YoutubeInfoController {

static const video = _VideoInfoController();
static const playlist = YoutiPie.playlist;
static final history = _YoutubeHistoryLinker(() => YoutiPie.activeAccountDetails.value?.id);
static const userplaylist = YoutiPie.userplaylist;
static const comment = YoutiPie.comment;
static const commentAction = YoutiPie.commentAction;
Expand All @@ -58,6 +64,7 @@ class YoutubeInfoController {
checkJSPlayer: false, // we properly check for jsplayer with each streams request if needed,
checkHasConnectionCallback: () => ConnectivityController.inst.hasConnection,
);
history.init(AppDirs.YOUTIPIE_CACHE);
YoutiPie.setLogs(_YTReportingLog());
}

Expand Down
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies:
vibration: ^1.8.4
flutter_displaymode: ^0.6.0
flutter_udid: ^3.0.0
namico_db_wrapper:
git:
url: https://github.com/namidaco/namico_db_wrapper

# ---- Audio Indexing & Playback ----
just_audio:
Expand Down

0 comments on commit 2d12b32

Please sign in to comment.