feat: support for importing watch history from YouTube

This commit is contained in:
Bnyro 2024-09-26 16:48:55 +02:00
parent 217d671a34
commit 5e556f4be2
10 changed files with 154 additions and 40 deletions

View File

@ -23,8 +23,8 @@ object DatabaseHelper {
videoId,
streams.toStreamItem(videoId)
)
suspend fun addToWatchHistory(videoId: String, stream: StreamItem) =
withContext(Dispatchers.IO) {
suspend fun addToWatchHistory(videoId: String, stream: StreamItem) {
val watchHistoryItem = WatchHistoryItem(
videoId,
stream.title,
@ -36,6 +36,12 @@ object DatabaseHelper {
stream.thumbnail,
stream.duration
)
addToWatchHistory(watchHistoryItem)
}
suspend fun addToWatchHistory(watchHistoryItem: WatchHistoryItem) =
withContext(Dispatchers.IO) {
Database.watchHistoryDao().insert(watchHistoryItem)
val maxHistorySize = PreferenceHelper.getString(
PreferenceKeys.WATCH_HISTORY_SIZE,

View File

@ -7,5 +7,6 @@ enum class ImportFormat(@StringRes val value: Int) {
NEWPIPE(R.string.import_format_newpipe),
FREETUBE(R.string.import_format_freetube),
YOUTUBECSV(R.string.import_format_youtube_csv),
YOUTUBEJSON(R.string.youtube),
PIPED(R.string.import_format_piped)
}

View File

@ -1,6 +1,7 @@
package com.github.libretube.helpers
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.util.Log
import com.github.libretube.R
@ -8,7 +9,9 @@ import com.github.libretube.api.JsonHelper
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher
@ -19,6 +22,7 @@ import com.github.libretube.obj.NewPipeSubscription
import com.github.libretube.obj.NewPipeSubscriptions
import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.obj.PipedPlaylistFile
import com.github.libretube.obj.YouTubeWatchHistoryFileItem
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.util.TextUtils
import kotlinx.serialization.ExperimentalSerializationApi
@ -27,6 +31,8 @@ import kotlinx.serialization.json.encodeToStream
import java.util.stream.Collectors
object ImportHelper {
private const val IMPORT_THUMBNAIL_QUALITY = "mqdefault"
/**
* Import subscriptions by a file uri
*/
@ -254,4 +260,43 @@ object ImportHelper {
else -> Unit
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun importWatchHistory(context: Context, uri: Uri, importFormat: ImportFormat) {
val videos = when (importFormat) {
ImportFormat.YOUTUBEJSON -> {
context.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<List<YouTubeWatchHistoryFileItem>>(it)
}
.orEmpty()
.filter { it.activityControls.contains("YouTube watch history") }
.reversed()
.map {
val videoId = it.titleUrl.substring(it.titleUrl.length - 11)
WatchHistoryItem(
videoId = videoId,
title = it.title.replaceFirst("Watched ", ""),
uploader = it.subtitles.firstOrNull()?.name,
uploaderUrl = it.subtitles.firstOrNull()?.url?.let { url ->
url.substring(url.length - 24)
},
thumbnailUrl = "https://img.youtube.com/vi/${videoId}/${IMPORT_THUMBNAIL_QUALITY}.jpg"
)
}
}
else -> emptyList()
}
for (video in videos) {
DatabaseHelper.addToWatchHistory(video)
}
if (videos.isEmpty()) {
context.toastFromMainDispatcher(R.string.emptyList)
} else {
context.toastFromMainDispatcher(R.string.success)
}
}
}

View File

@ -0,0 +1,9 @@
package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class YouTubeWatchHistoryChannelInfo(
val name: String,
val url: String
)

View File

@ -0,0 +1,14 @@
package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class YouTubeWatchHistoryFileItem(
val activityControls: List<String>,
val header: String,
val products: List<String>,
val subtitles: List<YouTubeWatchHistoryChannelInfo>,
val time: String,
val title: String,
val titleUrl: String
)

View File

@ -3,6 +3,7 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.VideoRowBinding
@ -54,9 +55,19 @@ class WatchHistoryAdapter(
videoTitle.text = video.title
channelName.text = video.uploader
videoInfo.text = video.uploadDate?.let { TextUtils.localizeDate(it) }
thumbnailDuration.setFormattedDuration(video.duration!!, null)
ImageHelper.loadImage(video.thumbnailUrl, thumbnail)
if (video.duration != null) {
thumbnailDuration.setFormattedDuration(video.duration, null)
} else {
thumbnailDurationCard.isGone = true
}
if (video.uploaderAvatar != null) {
ImageHelper.loadImage(video.uploaderAvatar, channelImage, true)
} else {
channelImage.isGone = true
}
channelImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
@ -81,7 +92,7 @@ class WatchHistoryAdapter(
true
}
watchProgress.setWatchProgressLength(video.videoId, video.duration)
if (video.duration != null) watchProgress.setWatchProgressLength(video.videoId, video.duration)
}
}
}

View File

@ -29,28 +29,25 @@ class BackupRestoreSettings : BasePreferenceFragment() {
private val backupDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")
private var backupFile = BackupFile()
private var importFormat: ImportFormat = ImportFormat.NEWPIPE
private val importSubscriptionFormatList
get() = listOf(
private val importSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportSubscriptionFormatList
get() = listOf(
private val exportSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE
)
private val importPlaylistFormatList
get() = listOf(
private val importPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportPlaylistFormatList
get() = listOf(
private val exportPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE
)
private val importWatchHistoryFormatList = listOf(ImportFormat.YOUTUBEJSON)
override val titleResourceId: Int = R.string.backup_restore
@ -94,9 +91,8 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}
/**
* result listeners for importing and exporting playlists
*/
// result listeners for importing and exporting playlists
private val getPlaylistsFile =
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
it?.forEach {
@ -106,6 +102,15 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}
private val getWatchHistoryFile =
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
it?.forEach {
CoroutineScope(Dispatchers.IO).launch {
ImportHelper.importWatchHistory(requireActivity(), it, importFormat)
}
}
}
private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
@ -179,6 +184,16 @@ class BackupRestoreSettings : BasePreferenceFragment() {
true
}
val importWatchHistory = findPreference<Preference>("import_watch_history")
importWatchHistory?.setOnPreferenceClickListener {
val list = importWatchHistoryFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.import_watch_history, list) {
importFormat = importWatchHistoryFormatList[it]
getWatchHistoryFile.launch(arrayOf("*/*"))
}
true
}
childFragmentManager.setFragmentResultListener(
BACKUP_DIALOG_REQUEST_KEY,
this

View File

@ -35,6 +35,7 @@
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<androidx.cardview.widget.CardView
android:id="@+id/thumbnail_duration_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"

View File

@ -361,6 +361,8 @@
<string name="all_caught_up_summary">You\'ve seen all new videos</string>
<string name="import_playlists">Import playlists</string>
<string name="export_playlists">Export playlists</string>
<string name="import_watch_history">Import watch history</string>
<string name="import_watch_history_desc">Please note that not everything will be imported due to YouTube\'s limited export data.</string>
<string name="app_backup">App Backup</string>
<string name="backup_restore_summary">Import &amp; export subscriptions, playlists, …</string>
<string name="exportsuccess">Exported.</string>
@ -474,6 +476,7 @@
<string name="import_format_newpipe" translatable="false">NewPipe</string>
<string name="import_format_freetube" translatable="false">FreeTube</string>
<string name="import_format_youtube_csv" translatable="false">YouTube (CSV)</string>
<string name="import_format_youtube_json" translatable="false">YouTube (JSON)</string>
<string name="home_tab_content">Home tab content</string>
<string name="show_search_suggestions">Show search suggestions</string>
<string name="audio_track_format">%1$s - %2$s</string>

View File

@ -31,6 +31,15 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/watch_history">
<Preference
android:icon="@drawable/ic_download_filled"
app:key="import_watch_history"
app:title="@string/import_watch_history" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/app_backup">
<Preference