Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions app/src/main/java/com/github/libretube/db/DatabaseHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,25 @@ object DatabaseHelper {
videoId,
streams.toStreamItem(videoId)
)
suspend fun addToWatchHistory(videoId: String, stream: StreamItem) =

suspend fun addToWatchHistory(videoId: String, stream: StreamItem) {
val watchHistoryItem = WatchHistoryItem(
videoId,
stream.title,
Instant.fromEpochMilliseconds(stream.uploaded)
.toLocalDateTime(TimeZone.currentSystemDefault()).date,
stream.uploaderName,
stream.uploaderUrl?.toID(),
stream.uploaderAvatar,
stream.thumbnail,
stream.duration
)

addToWatchHistory(watchHistoryItem)
}

suspend fun addToWatchHistory(watchHistoryItem: WatchHistoryItem) =
withContext(Dispatchers.IO) {
val watchHistoryItem = WatchHistoryItem(
videoId,
stream.title,
Instant.fromEpochMilliseconds(stream.uploaded)
.toLocalDateTime(TimeZone.currentSystemDefault()).date,
stream.uploaderName,
stream.uploaderUrl?.toID(),
stream.uploaderAvatar,
stream.thumbnail,
stream.duration
)
Database.watchHistoryDao().insert(watchHistoryItem)
val maxHistorySize = PreferenceHelper.getString(
PreferenceKeys.WATCH_HISTORY_SIZE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/github/libretube/helpers/ImportHelper.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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
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
Expand All @@ -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
Expand All @@ -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
*/
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.libretube.obj

import kotlinx.serialization.Serializable

@Serializable
data class YouTubeWatchHistoryChannelInfo(
val name: String,
val url: String
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
ImageHelper.loadImage(video.uploaderAvatar, channelImage, true)

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)
Expand All @@ -81,7 +92,7 @@ class WatchHistoryAdapter(
true
}

watchProgress.setWatchProgressLength(video.videoId, video.duration)
if (video.duration != null) watchProgress.setWatchProgressLength(video.videoId, video.duration)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportSubscriptionFormatList
get() = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE
)
private val importPlaylistFormatList
get() = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportPlaylistFormatList
get() = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE
)
private val importSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE
)
private val importPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE
)
private val importWatchHistoryFormatList = listOf(ImportFormat.YOUTUBEJSON)

override val titleResourceId: Int = R.string.backup_restore

Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/layout/video_row.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/import_export_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading