Release v0.2.36
This commit is contained in:
+5
-6
@@ -1,12 +1,11 @@
|
||||
## 0.2.36 — 2026-08-09
|
||||
- Improved: Browse all movies or TV shows alongside individual genres, with clearer navigation and paging.
|
||||
|
||||
## 0.2.35 — 2026-08-09
|
||||
- Improved: Genre browsing now opens from a simpler button on the Movies and TV Series pages.
|
||||
- General bug fixes and improvements.
|
||||
|
||||
## 0.2.34 — 2026-08-09
|
||||
- Added: Closing credits can move aside and speed up while the next episode is ready.
|
||||
- Added: More colour themes, including seasonal themes for the whole household.
|
||||
- Added: Missing subtitles can now be found through OpenSubtitles as well as Bazarr.
|
||||
- Improved: Genre browsing now shows the right titles and loads more as you scroll.
|
||||
- Improved: The home screen, search, subtitles and settings are clearer and easier to use.
|
||||
- General bug fixes and improvements.
|
||||
|
||||
## 0.2.33 — 2026-08-08
|
||||
- General bug fixes and improvements.
|
||||
|
||||
@@ -42,7 +42,7 @@ val projectNoticeText =
|
||||
|
||||
// A release workflow can derive the app version from its Git tag without editing the
|
||||
// source tree. Local builds keep using the checked-in default.
|
||||
val defaultVersionName = "0.2.35"
|
||||
val defaultVersionName = "0.2.36"
|
||||
val membyVersionName: String =
|
||||
(project.findProperty("memby.versionName") as String?)
|
||||
?.trim()
|
||||
|
||||
@@ -757,6 +757,54 @@ class EmbyRepository(private val settings: SettingsStore) {
|
||||
return GenrePage(items, offset, total)
|
||||
}
|
||||
|
||||
/**
|
||||
* One stable, paged slice of all films or all series.
|
||||
*
|
||||
* This deliberately mirrors [browseGenre] so switching between All and a genre never
|
||||
* changes the grid's ordering, user-data fields, or infinite-scroll behaviour.
|
||||
*/
|
||||
suspend fun browseLibrary(
|
||||
offset: Int = 0,
|
||||
limit: Int = GENRE_PAGE_SIZE,
|
||||
itemType: String,
|
||||
): GenrePage {
|
||||
val embyItemType = if (itemType.trim().equals("Series", ignoreCase = true)) "Series" else "Movie"
|
||||
if (ServerConfig.isGateway) {
|
||||
runCatching { requireGateway().libraryItems(offset, limit, embyItemType) }
|
||||
.onSuccess { page -> return GenrePage(page.items, offset, page.total) }
|
||||
.onFailure { error ->
|
||||
if (error is kotlinx.coroutines.CancellationException) throw error
|
||||
if (offset > 0) throw error
|
||||
// An older gateway has no whole-library route. Its home response is a
|
||||
// useful first shelf and, crucially, is not claimed to be pageable.
|
||||
val home = getHome(limit)
|
||||
val items = (home.rows.asSequence().flatMap { it.items.asSequence() } +
|
||||
home.continueWatching.asSequence() + home.nextUp.asSequence() +
|
||||
home.favorites.asSequence() + home.latestMovies.asSequence())
|
||||
.filter { it.type.equals(embyItemType, ignoreCase = true) }
|
||||
.distinctBy(BaseItem::id)
|
||||
.take(limit)
|
||||
.toList()
|
||||
return GenrePage(items, offset, items.size)
|
||||
}
|
||||
}
|
||||
val items = getHomeItems(
|
||||
params = mapOf(
|
||||
"IncludeItemTypes" to embyItemType,
|
||||
"Recursive" to "true",
|
||||
"StartIndex" to offset.toString(),
|
||||
"Limit" to limit.toString(),
|
||||
"SortBy" to "PremiereDate,SortName",
|
||||
"SortOrder" to "Descending",
|
||||
),
|
||||
fields = "ProductionYear,RunTimeTicks,SeriesName,PrimaryImageAspectRatio",
|
||||
imageTypes = "Backdrop,Primary,Logo",
|
||||
includeUserData = true,
|
||||
)
|
||||
val total = offset + items.size + if (items.size >= limit) 1 else 0
|
||||
return GenrePage(items, offset, total)
|
||||
}
|
||||
|
||||
/** Records a successful gateway search without affecting the direct Emby path. */
|
||||
suspend fun recordSearch(term: String) {
|
||||
if (ServerConfig.isGateway && term.trim().length >= 2) {
|
||||
|
||||
@@ -388,8 +388,7 @@ data class GatewayItems(
|
||||
)
|
||||
|
||||
/**
|
||||
* One page of `GET /v1/genres/{genre}/items` — browsing a genre rather than searching for
|
||||
* its name.
|
||||
* One page of `GET /v1/genres/{genre}/items` or `GET /v1/library/items`.
|
||||
*
|
||||
* [total] is what ends the scroll. A page shorter than [limit] ends it too, but a genre
|
||||
* whose last page happens to divide evenly would otherwise cost one more empty request to
|
||||
|
||||
@@ -85,6 +85,14 @@ interface GatewayApi {
|
||||
@Query("type") itemType: String? = null,
|
||||
): com.ponzischeme89.memby.data.model.GatewayGenrePage
|
||||
|
||||
/** One paged media-type shelf without a genre filter. */
|
||||
@GET("v1/library/items")
|
||||
suspend fun libraryItems(
|
||||
@Query("offset") offset: Int,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("type") itemType: String,
|
||||
): com.ponzischeme89.memby.data.model.GatewayGenrePage
|
||||
|
||||
@POST("v1/search/history")
|
||||
suspend fun recordSearch(@Body body: Map<String, String>)
|
||||
|
||||
|
||||
@@ -2021,22 +2021,20 @@ private fun MediaCard(
|
||||
if (item.userData?.played == true || item.isFavorite) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (item.userData?.played == true) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = "Watched",
|
||||
MediaStatusIcon(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
description = "Watched",
|
||||
tint = EmbyGreen,
|
||||
modifier = Modifier.size(19.dp),
|
||||
)
|
||||
}
|
||||
if (item.isFavorite) {
|
||||
Icon(
|
||||
Icons.Default.Favorite,
|
||||
contentDescription = "Favourite",
|
||||
tint = EmbyGreen,
|
||||
modifier = Modifier.size(19.dp),
|
||||
MediaStatusIcon(
|
||||
icon = Icons.Default.Favorite,
|
||||
description = "Favourite",
|
||||
tint = Color(0xFFFF6B81),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2097,6 +2095,29 @@ private fun MediaCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaStatusIcon(
|
||||
icon: ImageVector,
|
||||
description: String,
|
||||
tint: Color,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(29.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.Black.copy(alpha = 0.78f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.16f), RoundedCornerShape(10.dp)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = description,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScheduleStatusBadge(status: String, label: String, modifier: Modifier = Modifier) {
|
||||
val color = when (status) {
|
||||
|
||||
@@ -71,7 +71,7 @@ fun GenreBrowseLauncher(
|
||||
FocusScaleContainer(
|
||||
onFocused = onFocused,
|
||||
onClick = onOpen,
|
||||
contentDescription = "Browse ${if (mediaLabel == "movie") "movies" else mediaLabel} by genre",
|
||||
contentDescription = "Browse ${if (mediaLabel == "movie") "movies" else mediaLabel}",
|
||||
modifier = modifier
|
||||
.padding(horizontal = 36.dp, vertical = 4.dp)
|
||||
.width(156.dp)
|
||||
@@ -98,7 +98,7 @@ fun GenreBrowseLauncher(
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
"Genres",
|
||||
"Browse",
|
||||
color = if (focused) MembySurface else Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -118,7 +118,7 @@ fun GenreBrowseScreen(
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val mediaLabel = if (itemType.equals("Series", ignoreCase = true)) "TV series" else "movies"
|
||||
val mediaLabel = if (itemType.equals("Series", ignoreCase = true)) "TV shows" else "movies"
|
||||
val browseViewModel: GenreBrowseViewModel = viewModel(
|
||||
key = "genre-browse-${itemType.lowercase()}",
|
||||
factory = remember(itemType) {
|
||||
@@ -127,8 +127,12 @@ fun GenreBrowseScreen(
|
||||
)
|
||||
val state by browseViewModel.state.collectAsStateWithLifecycle()
|
||||
val gridState = rememberLazyGridState()
|
||||
val allLabel = remember(itemType) { allMediaLabel(itemType) }
|
||||
|
||||
LaunchedEffect(genres) { browseViewModel.setGenres(genres) }
|
||||
LaunchedEffect(state.selectedGenre) {
|
||||
if (state.selectedGenre != null) gridState.scrollToItem(0)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(32L)
|
||||
runCatching { contentFocusRequester.requestFocus() }
|
||||
@@ -178,13 +182,13 @@ fun GenreBrowseScreen(
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column {
|
||||
Text(
|
||||
"Browse $mediaLabel by genre",
|
||||
"Browse $mediaLabel",
|
||||
color = Color.White,
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
state.selectedGenre ?: "Choose a genre",
|
||||
state.selectedGenre?.takeIf(String::isNotEmpty) ?: allLabel,
|
||||
color = MembyMutedText,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
@@ -194,10 +198,12 @@ fun GenreBrowseScreen(
|
||||
Spacer(Modifier.height(16.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = horizontalPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
rowItemsIndexed(state.genres, key = { _, genre -> genre }) { index, genre ->
|
||||
val tabs = listOf(ALL_MEDIA) + state.genres
|
||||
rowItemsIndexed(tabs, key = { _, genre -> genre.ifEmpty { "all-media" } }) { index, genre ->
|
||||
val selected = genre == state.selectedGenre
|
||||
val label = genre.ifEmpty { allLabel }
|
||||
FocusScaleContainer(
|
||||
onFocused = {},
|
||||
onClick = {
|
||||
@@ -205,31 +211,38 @@ fun GenreBrowseScreen(
|
||||
browseViewModel.selectGenre(genre)
|
||||
}
|
||||
},
|
||||
contentDescription = "Browse $genre $mediaLabel",
|
||||
contentDescription = "Browse $label",
|
||||
modifier = Modifier
|
||||
.then(if (index == 0) Modifier.focusRequester(contentFocusRequester) else Modifier)
|
||||
.focusProperties { if (index == 0) left = navigationFocusRequester }
|
||||
.background(
|
||||
if (selected) MembyAccent.copy(alpha = 0.24f) else MembySurfaceRaised,
|
||||
RoundedCornerShape(10.dp),
|
||||
when {
|
||||
selected -> MembyAccent
|
||||
else -> MembySurfaceRaised.copy(alpha = 0.72f)
|
||||
},
|
||||
RoundedCornerShape(18.dp),
|
||||
)
|
||||
.border(
|
||||
1.dp,
|
||||
if (selected) MembyAccent else Color.White.copy(alpha = 0.08f),
|
||||
RoundedCornerShape(10.dp),
|
||||
if (selected) Color.White.copy(alpha = 0.18f) else Color.White.copy(alpha = 0.06f),
|
||||
RoundedCornerShape(18.dp),
|
||||
),
|
||||
) { focused ->
|
||||
Text(
|
||||
genre,
|
||||
color = if (focused || selected) Color.White else MembyMutedText,
|
||||
label,
|
||||
color = when {
|
||||
focused -> MembySurface
|
||||
selected -> Color.White
|
||||
else -> MembyMutedText
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.SemiBold,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (focused) Color.White.copy(alpha = 0.12f) else Color.Transparent,
|
||||
RoundedCornerShape(10.dp),
|
||||
if (focused) Color.White else Color.Transparent,
|
||||
RoundedCornerShape(18.dp),
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
.padding(horizontal = 17.dp, vertical = 9.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -237,13 +250,19 @@ fun GenreBrowseScreen(
|
||||
Spacer(Modifier.height(18.dp))
|
||||
|
||||
when {
|
||||
state.genres.isEmpty() -> GenreMessage("No genres are available for $mediaLabel yet.")
|
||||
state.isLoading && state.items.isEmpty() -> GenreMessage("Loading ${state.selectedGenre.orEmpty()} $mediaLabel…")
|
||||
!state.genresInitialised -> GenreMessage("Loading $mediaLabel…")
|
||||
state.isLoading && state.items.isEmpty() -> GenreMessage(
|
||||
if (state.selectedGenre.isNullOrEmpty()) "Loading $allLabel…"
|
||||
else "Loading ${state.selectedGenre} $mediaLabel…",
|
||||
)
|
||||
state.errorMessage != null && state.items.isEmpty() -> GenreRetry(
|
||||
message = state.errorMessage.orEmpty(),
|
||||
onRetry = browseViewModel::retry,
|
||||
)
|
||||
state.items.isEmpty() -> GenreMessage("No ${state.selectedGenre.orEmpty()} $mediaLabel were found.")
|
||||
state.items.isEmpty() -> GenreMessage(
|
||||
if (state.selectedGenre.isNullOrEmpty()) "No $mediaLabel were found."
|
||||
else "No ${state.selectedGenre} $mediaLabel were found.",
|
||||
)
|
||||
else -> {
|
||||
LaunchedEffect(gridState, state.items.size, state.canLoadMore) {
|
||||
snapshotFlow { gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 }
|
||||
|
||||
@@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
data class GenreBrowseUiState(
|
||||
val genres: List<String> = emptyList(),
|
||||
val genresInitialised: Boolean = false,
|
||||
/** Empty means the whole media type; null means the screen has not been initialised. */
|
||||
val selectedGenre: String? = null,
|
||||
val items: List<BaseItem> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
@@ -36,22 +38,16 @@ class GenreBrowseViewModel(
|
||||
|
||||
fun setGenres(genres: List<String>) {
|
||||
val normalised = normaliseGenres(genres)
|
||||
if (normalised == state.value.genres) return
|
||||
if (normalised.isEmpty()) {
|
||||
pageJob?.cancel()
|
||||
_state.value = GenreBrowseUiState()
|
||||
return
|
||||
}
|
||||
if (normalised == state.value.genres && state.value.genresInitialised) return
|
||||
val selected = state.value.selectedGenre?.takeIf { current ->
|
||||
normalised.any { it.equals(current, ignoreCase = true) }
|
||||
} ?: normalised.firstOrNull()
|
||||
_state.update { it.copy(genres = normalised) }
|
||||
if (selected != null && selected != state.value.selectedGenre) selectGenre(selected)
|
||||
current.isEmpty() || normalised.any { it.equals(current, ignoreCase = true) }
|
||||
} ?: ALL_MEDIA
|
||||
_state.update { it.copy(genres = normalised, genresInitialised = true) }
|
||||
if (selected != state.value.selectedGenre) selectGenre(selected)
|
||||
}
|
||||
|
||||
fun selectGenre(genre: String) {
|
||||
val selected = genre.trim()
|
||||
if (selected.isEmpty()) return
|
||||
pageJob?.cancel()
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -91,12 +87,16 @@ class GenreBrowseViewModel(
|
||||
|
||||
private suspend fun loadPage(genre: String, offset: Int) {
|
||||
runCatching {
|
||||
repository.browseGenre(
|
||||
genre = genre,
|
||||
offset = offset,
|
||||
limit = GENRE_PAGE_SIZE,
|
||||
itemType = itemType,
|
||||
)
|
||||
if (genre == ALL_MEDIA) {
|
||||
repository.browseLibrary(offset = offset, limit = GENRE_PAGE_SIZE, itemType = itemType)
|
||||
} else {
|
||||
repository.browseGenre(
|
||||
genre = genre,
|
||||
offset = offset,
|
||||
limit = GENRE_PAGE_SIZE,
|
||||
itemType = itemType,
|
||||
)
|
||||
}
|
||||
}.onSuccess { page ->
|
||||
_state.update { current ->
|
||||
if (current.selectedGenre != genre || current.items.size != page.offset) return@update current
|
||||
@@ -127,6 +127,11 @@ class GenreBrowseViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
const val ALL_MEDIA = ""
|
||||
|
||||
fun allMediaLabel(itemType: String): String =
|
||||
if (itemType.equals("Series", ignoreCase = true)) "All TV shows" else "All Movies"
|
||||
|
||||
fun normaliseGenres(genres: List<String>): List<String> = genres
|
||||
.asSequence()
|
||||
.map(String::trim)
|
||||
|
||||
@@ -4,6 +4,12 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GenreBrowseTest {
|
||||
@Test
|
||||
fun `whole library tab names match each destination`() {
|
||||
assertEquals("All Movies", allMediaLabel("Movie"))
|
||||
assertEquals("All TV shows", allMediaLabel("Series"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `genre picker trims deduplicates and sorts names`() {
|
||||
assertEquals(
|
||||
|
||||
@@ -139,6 +139,7 @@ func (s *Server) Routes() http.Handler {
|
||||
// A genre is browsed, not searched: the chip is a filter and this is the route that
|
||||
// treats it as one. Paged, because a household's Drama shelf is not a screenful.
|
||||
v1.Handle("GET /v1/genres/{genre}/items", s.authed(s.handleGenreItems))
|
||||
v1.Handle("GET /v1/library/items", s.authed(s.handleLibraryItems))
|
||||
v1.Handle("GET /v1/search/history", s.authed(s.handleRecentSearches))
|
||||
v1.Handle("POST /v1/search/history", s.authed(s.handleSearchHistory))
|
||||
v1.Handle("GET /v1/requests/lookup", s.authed(s.handleRequestLookup))
|
||||
|
||||
@@ -45,21 +45,38 @@ type genrePage struct {
|
||||
}
|
||||
|
||||
func (s *Server) handleGenreItems(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
ctx := r.Context()
|
||||
genre := strings.TrimSpace(r.PathValue("genre"))
|
||||
if genre == "" {
|
||||
writeError(w, http.StatusBadRequest, "a genre is required")
|
||||
return
|
||||
}
|
||||
s.handleBrowseItems(w, r, sess, genre)
|
||||
}
|
||||
|
||||
func (s *Server) handleLibraryItems(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
s.handleBrowseItems(w, r, sess, "")
|
||||
}
|
||||
|
||||
func (s *Server) handleBrowseItems(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
sess store.Session,
|
||||
genre string,
|
||||
) {
|
||||
ctx := r.Context()
|
||||
limit := queryInt(r, "limit", genrePageSize, genrePageMax)
|
||||
offset := queryOffset(r, "offset")
|
||||
itemType, ok := genreItemType(r.URL.Query().Get("type"))
|
||||
if !ok {
|
||||
if !ok || (genre == "" && itemType == "Movie,Series") {
|
||||
writeError(w, http.StatusBadRequest, "type must be Movie or Series")
|
||||
return
|
||||
}
|
||||
|
||||
key := cache.UserKey(sess.EmbyUserID, "genre:"+itemType+":"+genre+":"+itoa(offset)+":"+itoa(limit))
|
||||
filterKey := "all"
|
||||
if genre != "" {
|
||||
filterKey = "genre:" + genre
|
||||
}
|
||||
key := cache.UserKey(sess.EmbyUserID, "browse:"+itemType+":"+filterKey+":"+itoa(offset)+":"+itoa(limit))
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
@@ -67,7 +84,6 @@ func (s *Server) handleGenreItems(w http.ResponseWriter, r *http.Request, sess s
|
||||
}
|
||||
|
||||
params := rowParams(url.Values{
|
||||
"Genres": {genre},
|
||||
"IncludeItemTypes": {itemType},
|
||||
"Recursive": {"true"},
|
||||
"StartIndex": {itoa(offset)},
|
||||
@@ -79,6 +95,9 @@ func (s *Server) handleGenreItems(w http.ResponseWriter, r *http.Request, sess s
|
||||
"SortBy": {"PremiereDate,SortName"},
|
||||
"SortOrder": {"Descending"},
|
||||
}, fieldsRow)
|
||||
if genre != "" {
|
||||
params.Set("Genres", genre)
|
||||
}
|
||||
// rowParams turns this off for the home rows, which never page. Here it is the number
|
||||
// the scroll stops on.
|
||||
params.Set("EnableTotalRecordCount", "true")
|
||||
@@ -98,10 +117,14 @@ func (s *Server) handleGenreItems(w http.ResponseWriter, r *http.Request, sess s
|
||||
|
||||
// The first page is somebody opening a genre, which is a navigation event worth the
|
||||
// log; the pages after it are one viewer scrolling and would bury it.
|
||||
if offset == 0 {
|
||||
if offset == 0 && genre != "" {
|
||||
s.loggerFor(ctx).Info("genre browsed", "genre", genre, "results", len(items), "total", total)
|
||||
} else {
|
||||
} else if offset == 0 {
|
||||
s.loggerFor(ctx).Info("library browsed", "type", itemType, "results", len(items), "total", total)
|
||||
} else if genre != "" {
|
||||
s.loggerFor(ctx).Debug("genre page", "genre", genre, "offset", offset, "results", len(items))
|
||||
} else {
|
||||
s.loggerFor(ctx).Debug("library page", "type", itemType, "offset", offset, "results", len(items))
|
||||
}
|
||||
|
||||
body, err := json.Marshal(genrePage{
|
||||
|
||||
@@ -137,7 +137,7 @@ func componentFor(path string) string {
|
||||
return "settings"
|
||||
case path == "/v1/screensaver", path == "/v1/preroll":
|
||||
return "screensaver"
|
||||
case strings.HasPrefix(path, "/v1/search"), strings.HasPrefix(path, "/v1/genres/"):
|
||||
case strings.HasPrefix(path, "/v1/search"), strings.HasPrefix(path, "/v1/genres/"), path == "/v1/library/items":
|
||||
return "search"
|
||||
case strings.HasPrefix(path, "/v1/requests"):
|
||||
return "requests"
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestComponentNamesThePartOfTheAppARouteBelongsTo(t *testing.T) {
|
||||
"/v1/auth/devices/tv-1": "devices",
|
||||
"/v1/search": "search",
|
||||
"/v1/genres/Comedy/items": "search",
|
||||
"/v1/library/items": "search",
|
||||
"/v1/items/42": "details",
|
||||
"/v1/items/42/related": "details",
|
||||
"/v1/items/42/playback": "playback",
|
||||
|
||||
Reference in New Issue
Block a user