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
Original file line number Diff line number Diff line change
Expand Up @@ -11,192 +11,212 @@ import us.nineworlds.serenity.emby.server.model.NameGuidPair

class MediaContainerAdaptor {

companion object {
const val TICKS_PER_MILLISECOND: Long = 10000
}

fun createMainMenu(items: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val directories = ArrayList<Directory>()
for (item in items) {
val entry = Directory()
entry.title = item.name
entry.type = item.collectionType
entry.key = item.id
directories.add(entry)
companion object {
const val TICKS_PER_MILLISECOND: Long = 10000
}

mediaContainer.directories = directories.toList()
return mediaContainer
}
fun createMainMenu(items: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val directories = ArrayList<Directory>()
for (item in items) {
val entry = Directory()
entry.title = item.name
entry.type = item.collectionType
entry.key = item.id
directories.add(entry)
}

mediaContainer.directories = directories.toList()
return mediaContainer
}

fun createCategory(genres: List<NameGuidPair>, series: Boolean? = false): IMediaContainer {
val mediaContainer = MediaContainer()
val directories = ArrayList<Directory>()
fun createCategory(genres: List<NameGuidPair>, series: Boolean? = false): IMediaContainer {
val mediaContainer = MediaContainer()
val directories = ArrayList<Directory>()

directories.add(createAllCatagory())
if (series == false) {
directories.add(createUnwatched())
directories.add(createRecentlyAddedCategory())
directories.add(createOnDeck())
}

directories.add(createAllCatagory())
if (series == false) {
directories.add(createUnwatched())
directories.add(createRecentlyAddedCategory())
directories.add(createOnDeck())
for (genre in genres) {
val entry = Directory()
entry.title = genre.name
entry.key = genre.name
entry.secondary = 0
directories.add(entry)
}

mediaContainer.directories = directories.toList()

return mediaContainer
}

for (genre in genres) {
val entry = Directory()
entry.title = genre.name
entry.key = genre.name
entry.secondary = 0
directories.add(entry)
fun createAllCatagory(): Directory {
val allCategory = Directory()
allCategory.title = "All"
allCategory.key = "all"
return allCategory
}

mediaContainer.directories = directories.toList()

return mediaContainer
}

fun createAllCatagory(): Directory {
val allCategory = Directory()
allCategory.title = "All"
allCategory.key = "all"
return allCategory
}

fun createUnwatched(): Directory {
val allCategory = Directory()
allCategory.title = "Unwatched"
allCategory.key = "unwatched"
return allCategory
}

fun createRecentlyAddedCategory(): Directory {
val allCategory = Directory()
allCategory.title = "Recently Added"
allCategory.key = "recentlyAdded"
return allCategory
}

fun createOnDeck(): Directory {
val allCategory = Directory()
allCategory.title = "OnDeck"
allCategory.key = "ondeck"
return allCategory
}

fun createSeriesList(series: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val seriesVideos = ArrayList<IDirectory>()

for (item in series) {
val seriesEntry = Directory()

seriesEntry.title = item.name
seriesEntry.summary = item.oveview
seriesEntry.key = item.id
seriesEntry.contentRating = item.officialRating
seriesEntry.rating = (item.communityRating ?: 0.00).toString()

var totalItemCount = 0L
var viewdItemsCount = 0L
if (item.userData != null) {
if (item.userData.unplayedItemCount != null) {
totalItemCount += item.userData.unplayedItemCount
}
fun createUnwatched(): Directory {
val allCategory = Directory()
allCategory.title = "Unwatched"
allCategory.key = "unwatched"
return allCategory
}

fun createRecentlyAddedCategory(): Directory {
val allCategory = Directory()
allCategory.title = "Recently Added"
allCategory.key = "recentlyAdded"
return allCategory
}

fun createOnDeck(): Directory {
val allCategory = Directory()
allCategory.title = "OnDeck"
allCategory.key = "ondeck"
return allCategory
}

fun createSeriesList(series: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val seriesVideos = ArrayList<IDirectory>()

for (item in series) {
val seriesEntry = Directory()

seriesEntry.title = item.name
seriesEntry.summary = item.oveview
seriesEntry.key = item.id
seriesEntry.contentRating = item.officialRating
seriesEntry.rating = (item.communityRating ?: 0.00).toString()

if (item.userData.playCount != null) {
viewdItemsCount = item.userData.playCount
totalItemCount += viewdItemsCount
var totalItemCount = 0L
var viewdItemsCount = 0L
if (item.userData != null) {
if (item.userData.unplayedItemCount != null) {
totalItemCount += item.userData.unplayedItemCount
}

if (item.userData.playCount != null) {
viewdItemsCount = item.userData.playCount
totalItemCount += viewdItemsCount
}
}

seriesEntry.leafCount = totalItemCount.toString()
seriesEntry.viewedLeafCount = viewdItemsCount.toString()

seriesEntry.art = "/emby/Items/${item.id}/Images/Thumb"
seriesEntry.thumb = "/emby/Items/${item.id}/Images/Primary"
seriesEntry.banner = "/emby/Items/${item.id}/Images/Banner"

seriesVideos.add(seriesEntry)
}
}
mediaContainer.directories = seriesVideos
mediaContainer.size = series.size

seriesEntry.leafCount = totalItemCount.toString()
seriesEntry.viewedLeafCount = viewdItemsCount.toString()
return mediaContainer
}

seriesEntry.art = "/emby/Items/${item.id}/Images/Thumb"
seriesEntry.thumb = "/emby/Items/${item.id}/Images/Primary"
seriesEntry.banner = "/emby/Items/${item.id}/Images/Banner"
fun createSeaonsList(seaons: List<Item>): IMediaContainer {
return MediaContainer()
}

seriesVideos.add(seriesEntry)
fun createVideoList(videos: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val serenityVideos = ArrayList<Video>()
mediaContainer.size = videos.size

val items = videos.filter { item -> item.type != "Folder" }
.filterNot { item -> item.name == "TBA" }

for (item in items) {
val video = Video()

val sortEpisode = item.episodeNumber?.toInt() ?: 0

video.type = item.type
video.titleSort = sortEpisode.toString().padStart(3, '0')
video.title = item.name
video.key = item.id
video.parentKey = item.parentId
video.contentRating = item.officialRating
video.summary = item.oveview
video.rating = item.communityRating ?: 0.00
video.season = item.parentIndexNumber
video.seriesName = item.seriesName

if (item.type != null && item.type == "Episode") {
video.backgroundImageKey = "/emby/Items/${item.parentId}/Images/Backdrop"
video.parentThumbNailImageKey = "/emby/Items/${item.parentId}/Images/Primary"
} else {
video.backgroundImageKey = "/emby/Items/${item.id}/Images/Backdrop"
}
video.thumbNailImageKey = "/emby/Items/${item.id}/Images/Primary"
video.viewCount = item.userData?.playCount?.toInt() ?: 0
val offset = convertTicksToMilliseconds(item.userData?.playbackPositionTicks ?: 0)
video.viewOffset = offset
video.episode = item.episodeNumber

val container = if (item.container != null && item.container.contains(",")) {
item.container.substringBefore(",")
} else {
item.container
}
video.directPlayUrl =
"emby/Videos/${item.mediaSources?.get(0)?.id ?: item.id}/stream.$container?static=true"

if (item.runTimeTicks != null) {
val milliseconds = convertTicksToMilliseconds(item.runTimeTicks)
video.duration = milliseconds
}

video.medias = createPlayableLists(item)

serenityVideos.add(video)
}
mediaContainer.videos = serenityVideos.sortedBy { item -> item.titleSort }.toList()
return mediaContainer
}
Comment on lines +130 to 185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and examine the code at the specified lines
find . -type f -name "MediaContainerAdaptor.kt" -o -name "*MediaContainer*" | head -20

Repository: NineWorlds/serenity-android

Length of output: 1961


🏁 Script executed:

# List files in the emby-lib directory to understand the structure
git ls-files emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/ | head -30

Repository: NineWorlds/serenity-android

Length of output: 1116


🏁 Script executed:

# Read the actual MediaContainerAdaptor.kt file to verify the code
cat -n emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/adapters/MediaContainerAdaptor.kt

Repository: NineWorlds/serenity-android

Length of output: 9398


🏁 Script executed:

# Find and examine the Item model
find . -name "itemsmodel.kt" -o -name "*Item.kt" | grep -E "(emby|model)" | head -10

Repository: NineWorlds/serenity-android

Length of output: 242


🏁 Script executed:

# Read the Item model definition
cat -n emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/model/itemsmodel.kt

Repository: NineWorlds/serenity-android

Length of output: 5302


🏁 Script executed:

# Check if there are any tests for MediaContainerAdaptor
find . -name "*MediaContainerAdaptor*" -type f

Repository: NineWorlds/serenity-android

Length of output: 264


🏁 Script executed:

# Check for tests of createVideoList
find . -type f -name "*.kt" | xargs grep -l "createVideoList" 2>/dev/null

Repository: NineWorlds/serenity-android

Length of output: 442


🏁 Script executed:

# Check the JellyfinMediaContainerAdaptor for comparison
cat -n jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/adapters/JellyfinMediaContainerAdaptor.kt | head -220

Repository: NineWorlds/serenity-android

Length of output: 8083


Fix three robustness issues in video list creation

The code has three correctness concerns that should be addressed:

  1. IndexOutOfBoundsException on empty mediaSources
    item.mediaSources?.get(0)?.id throws if mediaSources is a non-null but empty list. Use firstOrNull() instead.

  2. mediaContainer.size mismatch
    Line 133 sets mediaContainer.size = videos.size before filtering. Lines 135–136 then filter out folders and "TBA" items, causing the reported size to exceed the actual video count. Set size to items.size after filtering.

  3. Null container produces stream.null
    When item.container is null, the URL becomes stream.null. Use a safe approach to append the extension only when container exists:

    val container = item.container?.let { raw ->
        if (raw.contains(",")) raw.substringBefore(",") else raw
    }
    val mediaSourceId = item.mediaSources?.firstOrNull()?.id ?: item.id
    val extension = container?.let { ".$it" } ?: ""
    video.directPlayUrl = "emby/Videos/$mediaSourceId/stream$extension?static=true"
🤖 Prompt for AI Agents
In
emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/adapters/MediaContainerAdaptor.kt
around lines 130 to 185, address three robustness issues: (1) replace
mediaSources?.get(0)?.id with a safe lookup using firstOrNull() so an empty
non-null list doesn't throw IndexOutOfBoundsException; (2) set
mediaContainer.size after filtering (use items.size) instead of using the
original videos.size so the reported size matches the actual list; (3) avoid
producing "stream.null" by handling a null container safely — extract the
container extension only when non-null (and handle comma-separated values),
build the mediaSourceId via firstOrNull() fallback to item.id, and append the
extension only if present when composing directPlayUrl.

mediaContainer.directories = seriesVideos
mediaContainer.size = series.size

return mediaContainer
}

fun createSeaonsList(seaons: List<Item>): IMediaContainer {
return MediaContainer()
}

fun createVideoList(videos: List<Item>): IMediaContainer {
val mediaContainer = MediaContainer()
val serenityVideos = ArrayList<Video>()
mediaContainer.size = videos.size

val items = videos.filter { item -> item.type != "Folder" }
.filterNot { item -> item.name == "TBA" }

for (item in items) {
val video = Video()

val sortEpisode = item.episodeNumber?.toInt() ?: 0

video.type = item.type
video.titleSort = sortEpisode.toString().padStart(3, '0')
video.title = item.name
video.key = item.id
video.parentKey = item.parentId
video.contentRating = item.officialRating
video.summary = item.oveview
video.rating = item.communityRating ?: 0.00
video.season = item.parentIndexNumber
video.seriesName = item.seriesName

if (item.type != null && item.type == "Episode") {
video.backgroundImageKey = "/emby/Items/${item.parentId}/Images/Backdrop"
video.parentThumbNailImageKey = "/emby/Items/${item.parentId}/Images/Primary"
} else {
video.backgroundImageKey = "/emby/Items/${item.id}/Images/Backdrop"
}
video.thumbNailImageKey = "/emby/Items/${item.id}/Images/Primary"
video.viewCount = item.userData?.playCount?.toInt() ?: 0
val offset = convertTicksToMilliseconds(item.userData?.playbackPositionTicks ?: 0)
video.viewOffset = offset
video.episode = item.episodeNumber

val container = if (item.container != null && item.container.contains(",")) {
item.container.substringBefore(",")
} else {
item.container
}
video.directPlayUrl = "emby/Videos/${item.mediaSources?.get(0)?.id ?: item.id}/stream.$container?static=true"

if (item.runTimeTicks != null) {
val milliseconds = convertTicksToMilliseconds(item.runTimeTicks)
video.duration = milliseconds
}

if (item.mediaStreams != null) {
val medias = ArrayList<Media>()
val media = Media()
for (mediaStream in item.mediaStreams) {
media.container = item.container
if (mediaStream.type == "Video") {
media.aspectRatio = mediaStream.aspectRatio
media.videoCodec = mediaStream.codec
} else if (mediaStream.type == "Audio") {
media.audioCodec = mediaStream.codec
media.audioChannels = mediaStream.channels
}
medias.add(media)

fun createPlayableLists(item: Item): List<Media> {
val medias = mutableListOf<Media>()
if (item.mediaStreams.isNullOrEmpty()) {
return medias
}

val videoStreams = item.mediaStreams.filter { it.type == "Video" }
val audioStreams = item.mediaStreams.filter { it.type == "Audio" }

// If there's no video or no audio, no valid combination can be made.
if (videoStreams.isEmpty() || audioStreams.isEmpty()) {
return medias
}

videoStreams.forEach { videoStream ->
audioStreams.forEach { audioStream ->
val media = Media() // Create a new object for each combination
media.container = item.container

// From video stream
media.aspectRatio = videoStream.aspectRatio
media.videoCodec = videoStream.codec

// From audio stream
media.audioCodec = audioStream.codec
media.audioChannels = audioStream.channels

medias.add(media)
}
}
video.medias = medias.toList()
}

serenityVideos.add(video)
return medias
}
Comment on lines +187 to 219
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "MediaContainerAdaptor.kt" --type f

Repository: NineWorlds/serenity-android

Length of output: 260


🏁 Script executed:

find . -name "MediaContainerAdaptor.kt" -type f -exec wc -l {} \;

Repository: NineWorlds/serenity-android

Length of output: 163


🏁 Script executed:

rg "class MediaContainerAdaptor" -A 5

Repository: NineWorlds/serenity-android

Length of output: 707


🏁 Script executed:

find . -path "*emby*" -name "*Adaptor*.kt" -type f

Repository: NineWorlds/serenity-android

Length of output: 159


🏁 Script executed:

cat -n emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/adapters/MediaContainerAdaptor.kt

Repository: NineWorlds/serenity-android

Length of output: 9398


🏁 Script executed:

find . -name "*.kt" -type f -exec grep -l "class Media" {} \;

Repository: NineWorlds/serenity-android

Length of output: 496


🏁 Script executed:

rg "Media\(" -A 3 | head -50

Repository: NineWorlds/serenity-android

Length of output: 1165


🏁 Script executed:

rg "\.container" --type kt -B 2 -A 2 | head -100

Repository: NineWorlds/serenity-android

Length of output: 97


🏁 Script executed:

find . -path "*emby-lib*" -name "*.kt" -type f | grep -i media

Repository: NineWorlds/serenity-android

Length of output: 159


🏁 Script executed:

rg "\.container\s*[=:]|container.*=" --type-list | head -5

Repository: NineWorlds/serenity-android

Length of output: 460


🏁 Script executed:

rg "\.container" -B 2 -A 2 | grep -E "(container|direct|play|capability)" | head -60

Repository: NineWorlds/serenity-android

Length of output: 5609


🏁 Script executed:

rg "isDirectPlaySupportedForContainer" -B 2 -A 10

Repository: NineWorlds/serenity-android

Length of output: 3664


Apply container normalization in createPlayableLists to match createVideoList handling

The code in createVideoList normalizes comma-separated container strings before use (lines 166-170), but createPlayableLists assigns the raw item.container directly to media.container (line 204). This inconsistency should be corrected to ensure predictable container values downstream.

Apply this normalization in createPlayableLists:

media.container = item.container?.let { raw ->
    if (raw.contains(",")) raw.substringBefore(",") else raw
}

This keeps the container handling consistent across both methods and prevents potential issues if future capability checks or direct-play logic depend on normalized container values.

🤖 Prompt for AI Agents
In
emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/adapters/MediaContainerAdaptor.kt
around lines 187 to 219, createPlayableLists assigns item.container directly to
media.container, causing inconsistent container values vs createVideoList; fix
by normalizing item.container the same way as createVideoList: if item.container
is null leave media.container null, otherwise if it contains a comma set
media.container to the substring before the first comma, else set it to the
original string (use safe null handling to avoid NPEs).

mediaContainer.videos = serenityVideos.sortedBy { item -> item.titleSort } .toList()
return mediaContainer
}

fun convertTicksToMilliseconds(ticks: Long): Long = ticks.div(TICKS_PER_MILLISECOND)
fun convertTicksToMilliseconds(ticks: Long): Long = ticks.div(TICKS_PER_MILLISECOND)
}
Loading