add search for tv shows

media items are currently not selectable, the app will crash
This commit is contained in:
Jannik 2021-12-27 22:50:29 +01:00
parent 63ce910ec5
commit 4fd6f9ca7e
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
8 changed files with 112 additions and 78 deletions

View File

@ -134,18 +134,20 @@ object Crunchyroll {
return browseResult
}
// // TODO locale de-DE, type
suspend fun search(query: String, n: Int = 10) {
/**
* TODO
*/
suspend fun search(query: String, n: Int = 10): SearchResult {
val searchEndpoint = "/content/v1/search"
val parameters = listOf("q" to query, "n" to n)
val parameters = listOf("q" to query, "n" to n, "locale" to locale, "type" to "series")
val result = request(searchEndpoint, parameters)
println("${result.component1()?.obj()?.get("total")}")
// TODO episodes have thumbnails as image, and not poster_tall/poster_tall,
// to work around this, for now only tv shows are supported
val test = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!)
println(test.items.size)
// TODO return
return result.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: NoneSearchResult
}
/**

View File

@ -13,9 +13,29 @@ enum class SortBy(val str: String) {
POPULARITY("popularity")
}
/**
* Search data type
*/
@Serializable
data class SearchResult(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<SearchCollection>
)
@Serializable
data class SearchCollection(
@SerialName("type") val type: String,
@SerialName("items") val items: List<Item>
)
val NoneSearchResult = SearchResult(0, emptyList())
@Serializable
data class BrowseResult(val total: Int, val items: List<Item>)
// the data class Item is used in browse and search
@Serializable
data class Item(
val id: String,
@ -38,7 +58,7 @@ data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<Lis
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
/**
* Series return type
* Series data type
*/
@Serializable
data class Series(

View File

@ -135,10 +135,10 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
private fun load() {
val time = measureTimeMillis {
// start the initial loading
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
.async {
launch { MetaDBController.list() }
}
// val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
// .async {
// launch { MetaDBController.list() }
// }
// load all saved stuff here
Preferences.load(this)
@ -153,8 +153,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
runBlocking { Crunchyroll.index() }
}
// if (EncryptedPreferences.password.isEmpty()) {
// showOnboarding()
// } else {
@ -174,7 +172,7 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
// }
// }
runBlocking { loadingJob.await() } // wait for initial loading to finish
// runBlocking { loadingJob.await() } // wait for initial loading to finish
}
Log.i(javaClass.name, "loading and login in $time ms")

View File

@ -147,8 +147,8 @@ class HomeFragment : Fragment() {
* * only update actual change and not all data (performance)
*/
fun updateMyListMedia() {
adapterMyList.updateMediaList(mapMyListToItemMedia())
adapterMyList.notifyDataSetChanged()
//adapterMyList.updateMediaList(mapMyListToItemMedia())
//adapterMyList.notifyDataSetChanged()
}
private fun mapMyListToItemMedia(): List<ItemMedia> {

View File

@ -45,7 +45,7 @@ class LibraryFragment : Fragment() {
adapter = MediaItemAdapter(itemList)
adapter.onItemClick = { mediaIdStr, _ ->
activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr))
activity?.showFragment(MediaFragment(mediaIdStr))
}
binding.recyclerMediaLibrary.adapter = adapter
@ -78,9 +78,7 @@ class LibraryFragment : Fragment() {
})
nextItemIndex += pageSize
adapter.updateMediaList(itemList)
adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize)
isLoading = false
}
}

View File

@ -7,16 +7,24 @@ import android.view.ViewGroup
import android.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.mosad.teapod.databinding.FragmentSearchBinding
import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.showFragment
class SearchFragment : Fragment() {
private lateinit var binding: FragmentSearchBinding
private var adapter : MediaItemAdapter? = null
private lateinit var adapter: MediaItemAdapter
private val itemList = arrayListOf<ItemMedia>()
private var searchJob: Job? = null
private var oldSearchQuery = ""
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentSearchBinding.inflate(inflater, container, false)
@ -29,10 +37,10 @@ class SearchFragment : Fragment() {
lifecycleScope.launch {
// create and set the adapter, needs context
context?.let {
adapter = MediaItemAdapter(emptyList()) // TODO
adapter!!.onItemClick = { mediaId, _ ->
adapter = MediaItemAdapter(itemList)
adapter.onItemClick = { mediaIdStr, _ ->
binding.searchText.clearFocus()
activity?.showFragment(MediaFragment("")) //(mediaId))
activity?.showFragment(MediaFragment(mediaIdStr))
}
binding.recyclerMediaSearch.adapter = adapter
@ -46,16 +54,65 @@ class SearchFragment : Fragment() {
private fun initActions() {
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
adapter?.filter?.filter(query)
adapter?.notifyDataSetChanged()
query?.let { search(it) }
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
adapter?.filter?.filter(newText)
adapter?.notifyDataSetChanged()
newText?.let { search(it) }
return false
}
})
}
private fun search(query: String) {
// if the query hasn't changed since the last successful search, return
if (query == oldSearchQuery) return
// cancel search job if one is already running
if (searchJob?.isActive == true) searchJob?.cancel()
searchJob = lifecycleScope.async {
// TODO maybe wait a few ms (500ms?) before searching, if the user inputs any other chars
val results = Crunchyroll.search(query, 50)
itemList.clear() // TODO needs clean up
// TODO add top results first heading
itemList.addAll(results.items[0].items.map { item ->
ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
})
// TODO currently only tv shows are supported, hence only the first items array
// should be always present
// // TODO add tv shows heading
// if (results.items.size >= 2) {
// itemList.addAll(results.items[1].items.map { item ->
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
// })
// }
//
// // TODO add movies heading
// if (results.items.size >= 3) {
// itemList.addAll(results.items[2].items.map { item ->
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
// })
// }
//
// // TODO add episodes heading
// if (results.items.size >= 4) {
// itemList.addAll(results.items[3].items.map { item ->
// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
// })
// }
adapter.notifyDataSetChanged()
//adapter.notifyItemRangeInserted(0, itemList.size)
// after successfully searching the query term, add it as old query, to make sure we
// don't search again if the query hasn't changed
oldSearchQuery = query
}
}
}

View File

@ -41,6 +41,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
println("loading crunchyroll media $crunchyId")
// TODO info also in browse result item
// TODO doesn't support search
mediaCrunchy = Crunchyroll.browsingCache.find { it ->
it.id == crunchyId
} ?: NoneItem
@ -61,7 +62,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
// use tmdb search to get media info TODO media type is hardcoded, use type info from browse result once implemented
mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media
val tmdbId = tmdbApiController.search(stripTitleInfo(mediaCrunchy.title), MediaType.TVSHOW)
val tmdbId = tmdbApiController.search(mediaCrunchy.title, MediaType.TVSHOW)
tmdbResult = when (MediaType.TVSHOW) {
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)

View File

@ -2,19 +2,14 @@ package org.mosad.teapod.util.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import org.mosad.teapod.databinding.ItemMediaBinding
import org.mosad.teapod.util.ItemMedia
import java.util.*
class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable {
class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>() {
var onItemClick: ((id: String, position: Int) -> Unit)? = null
private val filter = MediaFilter()
private var filteredMedia = initMedia.map { it.copy() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.MediaViewHolder {
return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
@ -22,21 +17,13 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
override fun onBindViewHolder(holder: MediaItemAdapter.MediaViewHolder, position: Int) {
holder.binding.root.apply {
holder.binding.textTitle.text = filteredMedia[position].title
Glide.with(context).load(filteredMedia[position].posterUrl).into(holder.binding.imagePoster)
holder.binding.textTitle.text = items[position].title
Glide.with(context).load(items[position].posterUrl).into(holder.binding.imagePoster)
}
}
override fun getItemCount(): Int {
return filteredMedia.size
}
override fun getFilter(): Filter {
return filter
}
fun updateMediaList(mediaList: List<ItemMedia>) {
filteredMedia = mediaList
return items.size
}
inner class MediaViewHolder(val binding: ItemMediaBinding) :
@ -44,40 +31,11 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
init {
binding.root.setOnClickListener {
onItemClick?.invoke(
filteredMedia[bindingAdapterPosition].id,
items[bindingAdapterPosition].id,
bindingAdapterPosition
)
}
}
}
inner class MediaFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterTerm = constraint.toString().lowercase(Locale.ROOT)
val results = FilterResults()
val filteredList = if (filterTerm.isEmpty()) {
initMedia
} else {
initMedia.filter {
it.title.lowercase(Locale.ROOT).contains(filterTerm)
}
}
results.values = filteredList
results.count = filteredList.size
return results
}
@Suppress("unchecked_cast")
/**
* suppressing unchecked cast is safe, since we only use Media
*/
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
filteredMedia = results?.values as List<ItemMedia>
notifyDataSetChanged()
}
}
}