diff --git a/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/EndScreenItem.java b/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/EndScreenItem.java new file mode 100644 index 000000000..0f36face5 --- /dev/null +++ b/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/EndScreenItem.java @@ -0,0 +1,20 @@ +package com.liskovsoft.mediaserviceinterfaces.data; + +public interface EndScreenItem { + int TYPE_VIDEO = 0; + int TYPE_CHANNEL = 1; + int TYPE_PLAYLIST = 2; + int TYPE_UNKNOWN = 3; + + int getType(); + String getTitle(); + String getMetadata(); + String getImageUrl(); + String getId(); // videoId, channelId, or playlistId + long getStartTimeMs(); + long getEndTimeMs(); + float getLeft(); + float getTop(); + float getWidth(); + float getAspectRatio(); +} \ No newline at end of file diff --git a/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/MediaItemMetadata.java b/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/MediaItemMetadata.java index a63399083..d2fd9600e 100644 --- a/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/MediaItemMetadata.java +++ b/mediaserviceinterfaces/src/main/java/com/liskovsoft/mediaserviceinterfaces/data/MediaItemMetadata.java @@ -34,4 +34,5 @@ public interface MediaItemMetadata { List getNotificationStates(); long getDurationMs(); String getBadgeText(); + List getEndScreenItems(); } diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/common/models/gen/CommonHelper.kt b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/common/models/gen/CommonHelper.kt index 6754e805d..646666d6f 100644 --- a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/common/models/gen/CommonHelper.kt +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/common/models/gen/CommonHelper.kt @@ -1,13 +1,14 @@ package com.liskovsoft.youtubeapi.common.models.gen import com.liskovsoft.googlecommon.common.helpers.YouTubeHelper -import com.liskovsoft.mediaserviceinterfaces.data.MediaItem -import com.liskovsoft.sharedutils.helpers.DateHelper import com.liskovsoft.youtubeapi.browse.v2.gen.getContinuationToken import com.liskovsoft.youtubeapi.browse.v2.gen.getThumbnails import com.liskovsoft.youtubeapi.browse.v2.gen.getVideoId import com.liskovsoft.googlecommon.common.helpers.ServiceHelper import com.liskovsoft.youtubeapi.next.v2.gen.getContinuationToken +import com.liskovsoft.youtubeapi.next.v1.models.EndScreenElement +import com.liskovsoft.mediaserviceinterfaces.data.MediaItem +import com.liskovsoft.sharedutils.helpers.DateHelper // A badge before the image private const val BADGE_STYLE_LIVE = "LIVE" @@ -402,4 +403,9 @@ internal fun ResponseContext.getSuggestToken(): String? = serviceTrackingParams? if (it?.service == SERVICE_SUGGEST) { it.params?.firstOrNull { it?.key == KEY_SUGGEST_TOKEN }?.value } else null -} \ No newline at end of file +} + +//////////// +// End Screen Support + +internal fun EndScreenElement.getThumbnails() = image?.thumbnails \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreen.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreen.java new file mode 100644 index 000000000..1e117fa85 --- /dev/null +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreen.java @@ -0,0 +1,12 @@ +package com.liskovsoft.youtubeapi.next.v1.models; + +import com.google.gson.annotations.SerializedName; + +public class EndScreen { + @SerializedName("endscreenRenderer") + private EndScreenRenderer mRenderer; + + public EndScreenRenderer getRenderer() { + return mRenderer; + } +} \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenElement.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenElement.java new file mode 100644 index 000000000..e676d7b12 --- /dev/null +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenElement.java @@ -0,0 +1,118 @@ +package com.liskovsoft.youtubeapi.next.v1.models; + +import com.google.gson.annotations.SerializedName; +import com.liskovsoft.googlecommon.common.models.V2.TextItem; +import com.liskovsoft.youtubeapi.common.models.gen.ThumbnailItem; + +public class EndScreenElement { + @SerializedName("style") + private String mStyle; // "CHANNEL", "VIDEO", "PLAYLIST" + + @SerializedName("image") + private ThumbnailItem mImage; + + @SerializedName("title") + private TextItem mTitle; + + @SerializedName("metadata") + private TextItem mMetadata; + + @SerializedName("left") + private float mLeft; + + @SerializedName("top") + private float mTop; + + @SerializedName("width") + private float mWidth; + + @SerializedName("aspectRatio") + private float mAspectRatio; + + @SerializedName("startMs") + private String mStartMs; + + @SerializedName("endMs") + private String mEndMs; + + @SerializedName("endpoint") + private Endpoint mEndpoint; + + public String getStyle() { + return mStyle; + } + + public ThumbnailItem getImage() { + return mImage; + } + + public TextItem getTitle() { + return mTitle; + } + + public TextItem getMetadata() { + return mMetadata; + } + + public float getLeft() { + return mLeft; + } + + public float getTop() { + return mTop; + } + + public float getWidth() { + return mWidth; + } + + public float getAspectRatio() { + return mAspectRatio; + } + + public String getStartMs() { + return mStartMs; + } + + public String getEndMs() { + return mEndMs; + } + + public Endpoint getEndpoint() { + return mEndpoint; + } + + public static class Endpoint { + @SerializedName("browseEndpoint") + private BrowseEndpoint mBrowseEndpoint; + + @SerializedName("watchEndpoint") + private WatchEndpoint mWatchEndpoint; + + public BrowseEndpoint getBrowseEndpoint() { + return mBrowseEndpoint; + } + + public WatchEndpoint getWatchEndpoint() { + return mWatchEndpoint; + } + + public static class BrowseEndpoint { + @SerializedName("browseId") + private String mBrowseId; // channelId or playlistId + + public String getBrowseId() { + return mBrowseId; + } + } + + public static class WatchEndpoint { + @SerializedName("videoId") + private String mVideoId; + + public String getVideoId() { + return mVideoId; + } + } + } +} \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenRenderer.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenRenderer.java new file mode 100644 index 000000000..ab13d52e8 --- /dev/null +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/models/EndScreenRenderer.java @@ -0,0 +1,22 @@ +package com.liskovsoft.youtubeapi.next.v1.models; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class EndScreenRenderer { + @SerializedName("elements") + private List mElements; + + public List getElements() { + return mElements; + } + + public static class ElementWrapper { + @SerializedName("endscreenElementRenderer") + private EndScreenElement mElement; + + public EndScreenElement getElement() { + return mElement; + } + } +} \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/result/WatchNextResult.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/result/WatchNextResult.java index c88fd73fb..c3fe76584 100644 --- a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/result/WatchNextResult.java +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v1/result/WatchNextResult.java @@ -9,6 +9,7 @@ import com.liskovsoft.youtubeapi.next.v1.models.Playlist; import com.liskovsoft.youtubeapi.next.v1.models.SuggestedSection; import com.liskovsoft.youtubeapi.next.v1.models.VideoOwner; +import com.liskovsoft.youtubeapi.next.v1.models.EndScreen; import java.util.List; @@ -35,6 +36,8 @@ public class WatchNextResult { private ItemWrapper mReplayItem; @JsonPath("$.transportControls.transportControlsRenderer") private ButtonStates mButtonStates; + @JsonPath("$.endscreen") + private EndScreen mEndScreen; public List getSuggestedSections() { return mSuggestedSections; @@ -71,4 +74,8 @@ public ButtonStates getButtonStates() { public VideoItem getVideoDetails() { return mVideoDetails; } + + public EndScreen getEndScreen() { + return mEndScreen; + } } diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v2/impl/MediaItemMetadataImpl.kt b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v2/impl/MediaItemMetadataImpl.kt index f0f271641..cc77db4fc 100644 --- a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v2/impl/MediaItemMetadataImpl.kt +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/next/v2/impl/MediaItemMetadataImpl.kt @@ -312,4 +312,8 @@ internal data class MediaItemMetadataImpl(private val watchNextResult: WatchNext override fun getBadgeText(): String? { return badgeTextItem } -} + + override fun getEndScreenItems(): MutableList? { + return null // V2 API doesn't support end screens yet + } +} \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeEndScreenItem.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeEndScreenItem.java new file mode 100644 index 000000000..1348071c3 --- /dev/null +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeEndScreenItem.java @@ -0,0 +1,129 @@ +package com.liskovsoft.youtubeapi.service.data; + +import com.liskovsoft.mediaserviceinterfaces.data.EndScreenItem; +import com.liskovsoft.sharedutils.helpers.Helpers; +import com.liskovsoft.youtubeapi.next.v1.models.EndScreenElement; +import com.liskovsoft.googlecommon.common.helpers.YouTubeHelper; + +import static com.liskovsoft.youtubeapi.common.models.gen.CommonHelperKt.getThumbnails; + +public class YouTubeEndScreenItem implements EndScreenItem { + private int mType; + private String mTitle; + private String mMetadata; + private String mImageUrl; + private String mId; + private long mStartTimeMs; + private long mEndTimeMs; + private float mLeft; + private float mTop; + private float mWidth; + private float mAspectRatio; + + public static YouTubeEndScreenItem from(EndScreenElement element) { + if (element == null) { + return null; + } + + YouTubeEndScreenItem item = new YouTubeEndScreenItem(); + + // Determine type + String style = element.getStyle(); + if ("VIDEO".equals(style)) { + item.mType = TYPE_VIDEO; + } else if ("CHANNEL".equals(style)) { + item.mType = TYPE_CHANNEL; + } else if ("PLAYLIST".equals(style)) { + item.mType = TYPE_PLAYLIST; + } else { + item.mType = TYPE_UNKNOWN; + } + + // Extract text content + item.mTitle = element.getTitle() != null ? element.getTitle().toString() : null; + item.mMetadata = element.getMetadata() != null ? element.getMetadata().toString() : null; + + // Extract image URL using Kotlin extension + if (element.getImage() != null) { + item.mImageUrl = YouTubeHelper.findOptimalResThumbnailUrl(getThumbnails(element)); + } + + // Extract timing + item.mStartTimeMs = Helpers.parseLong(element.getStartMs()); + item.mEndTimeMs = Helpers.parseLong(element.getEndMs()); + + // Extract positioning + item.mLeft = element.getLeft(); + item.mTop = element.getTop(); + item.mWidth = element.getWidth(); + item.mAspectRatio = element.getAspectRatio(); + + // Extract ID based on endpoint type + if (element.getEndpoint() != null) { + EndScreenElement.Endpoint endpoint = element.getEndpoint(); + + if (endpoint.getWatchEndpoint() != null) { + item.mId = endpoint.getWatchEndpoint().getVideoId(); + } else if (endpoint.getBrowseEndpoint() != null) { + item.mId = endpoint.getBrowseEndpoint().getBrowseId(); + } + } + + return item; + } + + @Override + public int getType() { + return mType; + } + + @Override + public String getTitle() { + return mTitle; + } + + @Override + public String getMetadata() { + return mMetadata; + } + + @Override + public String getImageUrl() { + return mImageUrl; + } + + @Override + public String getId() { + return mId; + } + + @Override + public long getStartTimeMs() { + return mStartTimeMs; + } + + @Override + public long getEndTimeMs() { + return mEndTimeMs; + } + + @Override + public float getLeft() { + return mLeft; + } + + @Override + public float getTop() { + return mTop; + } + + @Override + public float getWidth() { + return mWidth; + } + + @Override + public float getAspectRatio() { + return mAspectRatio; + } +} \ No newline at end of file diff --git a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeMediaItemMetadata.java b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeMediaItemMetadata.java index 5cf68ffe6..00e7fb9fe 100644 --- a/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeMediaItemMetadata.java +++ b/youtubeapi/src/main/java/com/liskovsoft/youtubeapi/service/data/YouTubeMediaItemMetadata.java @@ -6,11 +6,14 @@ import com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata; import com.liskovsoft.mediaserviceinterfaces.data.NotificationState; import com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo; +import com.liskovsoft.mediaserviceinterfaces.data.EndScreenItem; import com.liskovsoft.sharedutils.helpers.Helpers; import com.liskovsoft.sharedutils.mylogger.Log; import com.liskovsoft.youtubeapi.browse.v1.models.sections.Chip; import com.liskovsoft.youtubeapi.common.models.items.VideoItem; import com.liskovsoft.youtubeapi.next.v1.models.ButtonStates; +import com.liskovsoft.youtubeapi.next.v1.models.EndScreen; +import com.liskovsoft.youtubeapi.next.v1.models.EndScreenRenderer; import com.liskovsoft.youtubeapi.next.v1.models.SuggestedSection; import com.liskovsoft.youtubeapi.next.v1.models.VideoMetadata; import com.liskovsoft.youtubeapi.next.v1.models.VideoOwner; @@ -42,6 +45,7 @@ public class YouTubeMediaItemMetadata implements MediaItemMetadata { private List mSuggestions; private boolean mIsLive; private boolean mIsUpcoming; + private List mEndScreenItems; private PlaylistInfo mPlaylistInfo; public static YouTubeMediaItemMetadata from(WatchNextResult watchNextResult) { @@ -159,6 +163,21 @@ public static YouTubeMediaItemMetadata from(WatchNextResult watchNextResult) { } } + // Parse end screen + EndScreen endScreen = watchNextResult.getEndScreen(); + if (endScreen != null && endScreen.getRenderer() != null) { + EndScreenRenderer renderer = endScreen.getRenderer(); + if (renderer.getElements() != null) { + mediaItemMetadata.mEndScreenItems = new ArrayList<>(); + for (EndScreenRenderer.ElementWrapper wrapper : renderer.getElements()) { + YouTubeEndScreenItem item = YouTubeEndScreenItem.from(wrapper.getElement()); + if (item != null) { + mediaItemMetadata.mEndScreenItems.add(item); + } + } + } + } + return mediaItemMetadata; } @@ -325,4 +344,9 @@ public long getDurationMs() { public String getBadgeText() { return null; } + + @Override + public List getEndScreenItems() { + return mEndScreenItems; + } }