mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
feat: support for importing watch history from YouTube
This commit is contained in:
parent
217d671a34
commit
5e556f4be2
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class YouTubeWatchHistoryChannelInfo(
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
@ -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
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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 & 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>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user