Skip to content

feat: Added coroutine worker for importing watch history #7973

Open
SE-Bilal-Ahmad wants to merge 20 commits intolibre-tube:masterfrom
SE-Bilal-Ahmad:master
Open

feat: Added coroutine worker for importing watch history #7973
SE-Bilal-Ahmad wants to merge 20 commits intolibre-tube:masterfrom
SE-Bilal-Ahmad:master

Conversation

@SE-Bilal-Ahmad
Copy link
Copy Markdown
Contributor

@SE-Bilal-Ahmad SE-Bilal-Ahmad commented Nov 29, 2025

1- Previously a coroutine was executed which imported watch history from youtube but there was no way to know the status of the import history i have a added a coroutine worker whose job is to import the watch history with a foreground notification.

2- I have also added dagger hilt in order to inject dependencies. I could have used other approaches but that wouldnt have been a clean solution

3- For now only watch history is being imported through coroutine worker. The reason is that we are inserting watch item one by one but for subscriptions one complete list is inserted which makes it nearly impossible to know the status of those imports however i will still look for a way to achieve that and for playlists too

Bug #7907

@SE-Bilal-Ahmad
Copy link
Copy Markdown
Contributor Author

If someone wants i can add an ongoing notification for subscription and playlist imports however that will not be able to tell how much content to import has been left

Copy link
Copy Markdown
Member

@Bnyro Bnyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally think that the idea of putting the import work into a dedicated worker goes into the right direction, thanks for proposing.

However, we decided to not add Dagger/Hilt as a dependency, so please refactor your PR to work without it.

Apart from that I left some comments, please have a look at them. Without willing to attack you personally, this code looks a bit AI-generated .

We don't accept contributions that add much additional code that was AI-generated here. So please clarify that this is not the case or rewrite the AI-generated parts as we don't have any permission to license code that was stolen from other projects as GPL3.

Comment thread app/src/main/java/com/github/libretube/factory/NotificationFactory.kt Outdated
Comment thread app/src/main/java/com/github/libretube/factory/NotificationFactory.kt Outdated
Comment thread app/src/main/java/com/github/libretube/helpers/ImportHelper.kt
Comment thread app/src/main/java/com/github/libretube/handler/ImportHandler.kt
Comment thread app/src/main/java/com/github/libretube/LibreTubeApp.kt Outdated
Comment thread app/src/main/java/com/github/libretube/factory/NotificationFactory.kt Outdated
Comment thread app/src/main/java/com/github/libretube/factory/NotificationFactory.kt Outdated
Comment thread app/src/main/java/com/github/libretube/receivers/ImportReceiver.kt
Comment thread app/src/main/java/com/github/libretube/receivers/ImportReceiver.kt
Comment thread gradle.properties Outdated
@SE-Bilal-Ahmad
Copy link
Copy Markdown
Contributor Author

SE-Bilal-Ahmad commented Dec 12, 2025

@Bnyro do i need to rewrite the other code as well by that i meant the pausing handler since thats not AI generated the only Ai generated one is the notification part that too is in much smaller portion not all of it was Ai generated so please clarify

@Bnyro
Copy link
Copy Markdown
Member

Bnyro commented Dec 12, 2025

Only the AI generated code.

@SE-Bilal-Ahmad
Copy link
Copy Markdown
Contributor Author

okay i will be rewriting it

@ULTRA-LEGENDS
Copy link
Copy Markdown
Contributor

@Bnyro could you please take a look at this pull request and see whether it’s ready to be merged into the app or not

private const val YOUTUBE_IMG_URL = "https://img.youtube.com"
private val IMPORT_THUMBNAIL_QUALITY = "mqdefault"
private val VIDEO_ID_LENGTH = 11
private val YOUTUBE_IMG_URL = "https://img.youtube.com"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove const here?

Comment thread build.gradle.kts
classpath(libs.kotlin.serialization)
classpath(libs.androidx.navigation.safeargs)

classpath("com.google.dagger:hilt-android-gradle-plugin:2.41")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this please, we don't want to use dagger hilt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have an ic_pause.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have an ic_play.

Comment on lines +23 to +27
val parsedId: UUID? = try {
UUID.fromString(id)
} catch (e: Exception) {
null
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val parsedId: UUID? = try {
UUID.fromString(id)
} catch (e: Exception) {
null
}
val parsedId runCatching {
UUID.fromString(id)
}.getOrNull()

Comment on lines +40 to +42
private val IMPORT_THUMBNAIL_QUALITY = "mqdefault"
private val VIDEO_ID_LENGTH = 11
private val YOUTUBE_IMG_URL = "https://img.youtube.com"
Copy link
Copy Markdown
Member

@Bnyro Bnyro Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make all these values const and move them to the companion object.

}
}?.flatten().orEmpty()

registerReceiver()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why register the receiver again here? It's already registered, right?

val importFormatString =
inputData.getString(WorkersData.IMPORT_FORMAT) ?: ImportFormat.YOUTUBEJSON.toString()
val importFormat = enumValueOf<ImportFormat>(importFormatString)
val videos = fileUrisString?.map { a ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val videos = fileUrisString?.map { a ->
val videos = fileUrisString?.flatMap { a ->

and remove flatten at the end.

inputData.getString(WorkersData.IMPORT_FORMAT) ?: ImportFormat.YOUTUBEJSON.toString()
val importFormat = enumValueOf<ImportFormat>(importFormatString)
val videos = fileUrisString?.map { a ->
val fileUrisDeserialized = Uri.parse(a)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val fileUrisDeserialized = Uri.parse(a)
val fileUri = a.toUri()

val importFormatString =
inputData.getString(WorkersData.IMPORT_FORMAT) ?: ImportFormat.YOUTUBEJSON.toString()
val importFormat = enumValueOf<ImportFormat>(importFormatString)
val videos = fileUrisString?.map { a ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a more descriptive name instead of a.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove: unused

setForeground(getForegroundInfo())
val pausingHandle = ImportHandler()
val importType = inputData.getString(WorkersData.IMPORT_TYPE)
?: ImportType.IMPORT_WATCH_HISTORY.toString()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no import type is provided, we should probably throw an error instead of just assuming that it's IMPORT_WATCH_HISTORY.

}


private suspend fun ImportWatchHistory(): Unit {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private suspend fun ImportWatchHistory(): Unit {
private suspend fun importWatchHistory(): Unit {

method names should be lowercase

}
}

private fun notifyNotification(notification: Notification) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private fun notifyNotification(notification: Notification) {
private fun updateNotification(notification: Notification) {

}

companion object {
fun importWatchHistory(context: Context,uris: List<Uri>, importFormat: ImportFormat) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fun importWatchHistory(context: Context,uris: List<Uri>, importFormat: ImportFormat) {
fun startImportWatchHistoryWorker(context: Context,uris: List<Uri>, importFormat: ImportFormat) {

Comment on lines +120 to +138
private suspend fun checkIfPausedOrCancelled(
currentState: Int,
finalState: Int,
dispatch: suspend (Boolean) -> Unit
) {
val importHandler = ImportHandler.current()
val now = System.currentTimeMillis()
if (importHandler.isPaused) {
dispatch(false)
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.PAUSED))
importHandler.awaitResumed()
} else if (!importHandler.isPaused) {
dispatch(true)
if (now - lastUpdateTime >= updateInterval) {
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.RUNNING))
lastUpdateTime = now
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand what this method is doing, in any case its name is not very descriptive.

As I understand it, the method is supposed to only execute the work from dispatch if ImportHandler#paused is false. However, in this case, why don't you just do

Suggested change
private suspend fun checkIfPausedOrCancelled(
currentState: Int,
finalState: Int,
dispatch: suspend (Boolean) -> Unit
) {
val importHandler = ImportHandler.current()
val now = System.currentTimeMillis()
if (importHandler.isPaused) {
dispatch(false)
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.PAUSED))
importHandler.awaitResumed()
} else if (!importHandler.isPaused) {
dispatch(true)
if (now - lastUpdateTime >= updateInterval) {
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.RUNNING))
lastUpdateTime = now
}
}
}
private suspend fun runActionAndUpdateNotificationIfNotPaused(
currentState: Int,
finalState: Int,
action: suspend () -> Unit
) {
val importHandler = ImportHandler.current()
val now = System.currentTimeMillis()
if (importHandler.isPaused) {
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.PAUSED))
importHandler.awaitResumed()
} else if (!importHandler.isPaused) {
action()
if (now - lastUpdateTime >= updateInterval) {
notifyNotification(notificationFactory.updateState(currentState, finalState, ImportState.RUNNING))
lastUpdateTime = now
}
}
}

Comment on lines +110 to +111
if (videos.isEmpty()) {
applicationContext.toastFromMainDispatcher(R.string.emptyList)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do this check before you execute the while loop for readability.



var lastUpdateTime = 0L
val updateInterval = 500L
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make update interval a constant in the companion object.

private var importFormat: ImportFormat = ImportFormat.NEWPIPE


override val titleResourceId: Int = R.string.backup_restore
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this change, perhaps this was by accident?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants