diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index c1171d0..cac61fa 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -211,6 +211,30 @@ object Crunchyroll { } ?: NoneSearchResult } + /** + * Get a collection of series objects. + * Note: episode objects are currently not supported + * + * @param objects The object IDs as list of Strings + * @return A Collection of Panels + */ + suspend fun objects(objects: List): Collection { + val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}" + val parameters = listOf( + "locale" to locale, + "Signature" to signature, + "Policy" to policy, + "Key-Pair-Id" to keyPairID + ) + + val result = request(episodesEndpoint, parameters) + println(result.component1()?.obj()?.toString()) + + return result.component1()?.obj()?.let { + json.decodeFromString(it.toString()) + } ?: NoneCollection + } + /** * series id == crunchyroll id? */ @@ -273,7 +297,7 @@ object Crunchyroll { } /** - * Additional media functions: watchlist, playhead + * Additional media functions: watchlist (series), playhead */ /** @@ -283,10 +307,10 @@ object Crunchyroll { * @return Boolean: ture if it was found, else false */ suspend fun isWatchlist(seriesId: String): Boolean { - val watchlistEndpoint = "/content/v1/watchlist/$accountID/$seriesId" + val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId" val parameters = listOf("locale" to locale) - val result = request(watchlistEndpoint, parameters) + val result = request(watchlistSeriesEndpoint, parameters) // if needed implement parsing return result.component1()?.obj()?.has(seriesId) ?: false @@ -298,14 +322,14 @@ object Crunchyroll { * @param seriesId The crunchyroll series id of the media to check */ suspend fun postWatchlist(seriesId: String) { - val watchlistEndpoint = "/content/v1/watchlist/$accountID" + val watchlistPostEndpoint = "/content/v1/watchlist/$accountID" val parameters = listOf("locale" to locale) val json = buildJsonObject { put("content_id", seriesId) } - requestPost(watchlistEndpoint, parameters, json.toString()) + requestPost(watchlistPostEndpoint, parameters, json.toString()) } /** @@ -314,10 +338,10 @@ object Crunchyroll { * @param seriesId The crunchyroll series id of the media to check */ suspend fun deleteWatchlist(seriesId: String) { - val watchlistEndpoint = "/content/v1/watchlist/$accountID/$seriesId" + val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId" val parameters = listOf("locale" to locale) - requestDelete(watchlistEndpoint, parameters) + requestDelete(watchlistDeleteEndpoint, parameters) } /** @@ -327,4 +351,21 @@ object Crunchyroll { // implement } + /** + * Listing functions: watchlist (list), up_next_account + */ + + suspend fun watchlist(n: Int = 20): Watchlist { + val watchlistEndpoint = "/content/v1/$accountID/watchlist" + val parameters = listOf("locale" to locale, "n" to n) + + val watchlistResult = request(watchlistEndpoint, parameters) + val list: ContinueWatchingList = watchlistResult.component1()?.obj()?.let { + json.decodeFromString(it.toString()) + } ?: NoneContinueWatchingList + + val objects = list.items.map{ it.panel.episodeMetadata.seriesId } + return objects(objects) + } + } diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 744ba52..34470c1 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -15,8 +15,18 @@ enum class SortBy(val str: String) { } /** - * Search data type + * search, browse, watchlist data types (all collections) */ +@Serializable +data class Collection( + @SerialName("total") val total: Int, + @SerialName("items") val items: List +) + +typealias SearchCollection = Collection +typealias BrowseResult = Collection +typealias Watchlist = Collection + @Serializable data class SearchResult( @SerialName("total") val total: Int, @@ -24,17 +34,21 @@ data class SearchResult( ) @Serializable -data class SearchCollection( - @SerialName("type") val type: String, - @SerialName("items") val items: List +data class ContinueWatchingList( + @SerialName("total") val total: Int, + @SerialName("items") val items: List ) -val NoneSearchResult = SearchResult(0, emptyList()) - - - @Serializable -data class BrowseResult(val total: Int, val items: List) +data class ContinueWatchingItem( + @SerialName("panel") val panel: EpisodePanel, + @SerialName("new") val new: Boolean, + @SerialName("new_content") val newContent: Boolean, + @SerialName("is_favorite") val isFavorite: Boolean, + @SerialName("never_watched") val neverWatched: Boolean, + @SerialName("completion_status") val completionStatus: Boolean, + @SerialName("playhead") val playhead: Int, +) // the data class Item is used in browse and search // TODO rename to MediaPanel @@ -49,8 +63,17 @@ data class Item( // TODO metadata etc. ) -val NoneItem = Item("", "", "", "", "", Images(listOf(), listOf())) -val NoneBrowseResult = BrowseResult(0, listOf()) +// EpisodePanel is used in ContinueWatchingItem +@Serializable +data class EpisodePanel( + @SerialName("id") val id: String, + @SerialName("title") val title: String, + @SerialName("type") val type: String, + @SerialName("channel_id") val channelId: String, + @SerialName("description") val description: String, + @SerialName("images") val images: Thumbnail, + @SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata, +) @Serializable data class Images(val poster_tall: List>, val poster_wide: List>) @@ -59,6 +82,18 @@ data class Images(val poster_tall: List>, val poster_wide: List if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.also { uri -> - StorageController.exportMyList(requireContext(), uri) + //StorageController.exportMyList(requireContext(), uri) } } } @@ -40,13 +39,13 @@ class AccountFragment : Fragment() { private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.also { uri -> - val success = StorageController.importMyList(requireContext(), uri) - if (success == 0) { - Toast.makeText( - context, getString(R.string.import_data_success), - Toast.LENGTH_SHORT - ).show() - } +// val success = StorageController.importMyList(requireContext(), uri) +// if (success == 0) { +// Toast.makeText( +// context, getString(R.string.import_data_success), +// Toast.LENGTH_SHORT +// ).show() +// } } } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 538d8fa..a39dc88 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -6,22 +6,21 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentHomeBinding +import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.util.ItemMedia -import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.decoration.MediaItemDecoration -import org.mosad.teapod.util.setDrawableTop import org.mosad.teapod.util.showFragment class HomeFragment : Fragment() { private lateinit var binding: FragmentHomeBinding - private lateinit var adapterMyList: MediaItemAdapter + private lateinit var adapterWatchlist: MediaItemAdapter private lateinit var adapterNewEpisodes: MediaItemAdapter - private lateinit var adapterNewSimulcasts: MediaItemAdapter private lateinit var adapterNewTitles: MediaItemAdapter private lateinit var adapterTopTen: MediaItemAdapter @@ -61,33 +60,37 @@ class HomeFragment : Fragment() { // } } - private fun initRecyclerViews() { - binding.recyclerMyList.addItemDecoration(MediaItemDecoration(9)) + /** + * Suspend, since adapters need to be initialized before we can initialize the actions. + */ + private suspend fun initRecyclerViews() { + binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9)) binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9)) - binding.recyclerNewSimulcasts.addItemDecoration(MediaItemDecoration(9)) binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9)) binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9)) - // my list - adapterMyList = MediaItemAdapter(mapMyListToItemMedia()) - binding.recyclerMyList.adapter = adapterMyList + val asyncJobList = arrayListOf() - // TODO - // new episodes + // watchlist + val watchlistJob = lifecycleScope.launch { + adapterWatchlist = MediaItemAdapter(mapMyListToItemMedia()) + binding.recyclerWatchlist.adapter = adapterWatchlist + } + asyncJobList.add(watchlistJob) + + // new episodes TODO replace with continue watching // adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList) // binding.recyclerNewEpisodes.adapter = adapterNewEpisodes - // new simulcasts -// adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList) -// binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts - - // new titles + // new titles TODO // adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList) // binding.recyclerNewTitles.adapter = adapterNewTitles - // top ten + // top ten TODO // adapterTopTen = MediaItemAdapter(AoDParser.topTenList) // binding.recyclerTopTen.adapter = adapterTopTen + + asyncJobList.joinAll() } private fun initActions() { @@ -103,23 +106,23 @@ class HomeFragment : Fragment() { } binding.textHighlightMyList.setOnClickListener { - if (StorageController.myList.contains(0)) { - StorageController.myList.remove(0) - binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) - } else { - StorageController.myList.add(0) - binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) - } - StorageController.saveMyList(requireContext()) + // TODO implement if needed - updateMyListMedia() // update my list, since it has changed +// if (StorageController.myList.contains(0)) { +// StorageController.myList.remove(0) +// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) +// } else { +// StorageController.myList.add(0) +// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) +// } +// StorageController.saveMyList(requireContext()) } binding.textHighlightInfo.setOnClickListener { activity?.showFragment(MediaFragment("")) } - adapterMyList.onItemClick = { id, _ -> + adapterWatchlist.onItemClick = { id, _ -> activity?.showFragment(MediaFragment("")) //(mediaId)) } @@ -140,27 +143,10 @@ class HomeFragment : Fragment() { // } } - /** - * update my media list - * TODO - * * auto call when StorageController.myList is changed - * * only update actual change and not all data (performance) - */ - fun updateMyListMedia() { - //adapterMyList.updateMediaList(mapMyListToItemMedia()) - //adapterMyList.notifyDataSetChanged() - } - - private fun mapMyListToItemMedia(): List { - return emptyList() -// return StorageController.myList.mapNotNull { elementId -> -// AoDParser.guiMediaList.firstOrNull { it.id == elementId.toString() }.also { -// // it the my list entry wasn't found in itemMediaList Log it -// if (it == null) { -// Log.w(javaClass.name, "The element with the id $elementId was not found.") -// } -// } -// } + private suspend fun mapMyListToItemMedia(): List { + return Crunchyroll.watchlist(50).items.map { + ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt index 2a5c11f..2ffb770 100644 --- a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt +++ b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt @@ -29,6 +29,9 @@ import kotlinx.coroutines.* import java.io.FileNotFoundException import java.net.URL +/** + * TODO remove gson usage + */ class MetaDBController { companion object { diff --git a/app/src/main/java/org/mosad/teapod/util/StorageController.kt b/app/src/main/java/org/mosad/teapod/util/StorageController.kt deleted file mode 100644 index 14a0f26..0000000 --- a/app/src/main/java/org/mosad/teapod/util/StorageController.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.mosad.teapod.util - -import android.content.Context -import android.net.Uri -import android.util.Log -import com.google.gson.Gson -import com.google.gson.JsonParser -import kotlinx.coroutines.* -import java.io.File -import java.io.FileReader -import java.io.FileWriter - -/** - * This controller contains the logic for permanently saved data. - * On load, it loads the saved files into the variables - */ -object StorageController { - - private const val fileNameMyList = "my_list.json" - - val myList = ArrayList() // a list of saved mediaIds - - fun load(context: Context) { - loadMyList(context) - } - - fun loadMyList(context: Context) { - val file = File(context.filesDir, fileNameMyList) - - if (!file.exists()) runBlocking { saveMyList(context).join() } - - try { - myList.clear() - myList.addAll(JsonParser.parseString(file.readText()).asJsonArray.map { it.asInt }.distinct()) - } catch (ex: Exception) { - myList.clear() - Log.e(javaClass.name, "Parsing of My-List failed.") - } - } - - fun saveMyList(context: Context): Job { - val file = File(context.filesDir, fileNameMyList) - - return CoroutineScope(Dispatchers.IO).launch { - file.writeText(Gson().toJson(myList.distinct())) - } - } - - fun exportMyList(context: Context, uri: Uri) { - try { - context.contentResolver.openFileDescriptor(uri, "w")?.use { - FileWriter(it.fileDescriptor).use { writer -> - writer.write(Gson().toJson(myList.distinct())) - } - } - } catch (ex: Exception) { - Log.e(javaClass.name, "Exporting my list failed.", ex) - } - } - - /** - * import my list from a (previously exported) json file - * @param context the current context - * @param uri the uri of the selected file - * @return 0 if import was successfull, else 1 - */ - fun importMyList(context: Context, uri: Uri): Int { - try { - val text = context.contentResolver.openFileDescriptor(uri, "r")?.use { - FileReader(it.fileDescriptor).use { reader -> - reader.readText() - } - } - - myList.clear() - myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct()) - - // after the list has been imported also save it - saveMyList(context) - } catch (ex: Exception) { - myList.clear() - Log.e(javaClass.name, "Importing my list failed.", ex) - return 1 - } - - return 0 - } - -} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 5a619ed..9e80ae8 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -108,14 +108,14 @@ - - - - - - -