diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69d6e47..0d57596 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { //----------media3 implementation("androidx.media3:media3-exoplayer:1.4.1") + implementation("androidx.media3:media3-exoplayer-dash:1.4.1") implementation("androidx.media3:media3-session:1.4.1") + implementation("androidx.media3:media3-ui:1.4.1") //----------media3 } \ No newline at end of file diff --git a/app/src/main/java/com/hi/music/player/api/MediaControllerListener.java b/app/src/main/java/com/hi/music/player/api/MediaControllerListener.java new file mode 100644 index 0000000..0b7f511 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/api/MediaControllerListener.java @@ -0,0 +1,9 @@ +package com.hi.music.player.api; + +public interface MediaControllerListener { + + + void onMediaControllerComplete(boolean isOk); + + void onPlayStatus(int playStatus); +} diff --git a/app/src/main/java/com/hi/music/player/helper/CommonUtils.java b/app/src/main/java/com/hi/music/player/helper/CommonUtils.java index b96d329..4d0fc47 100644 --- a/app/src/main/java/com/hi/music/player/helper/CommonUtils.java +++ b/app/src/main/java/com/hi/music/player/helper/CommonUtils.java @@ -62,4 +62,22 @@ public class CommonUtils { } return statusBarHeight; } + + + + //time 3:45 + public static long convertToMilliseconds(String time) { + String[] parts = time.split(":"); + int minutes = Integer.parseInt(parts[0]); + int seconds = Integer.parseInt(parts[1]); + + return (minutes * 60 + seconds) * 1000; // 转换为毫秒 + } + + + public static String convertMillisToTime(long millis) { + long seconds = (millis / 1000) % 60; + long minutes = (millis / (1000 * 60)) % 60; + return String.format("%d:%02d", minutes, seconds); // 格式化为 mm:ss + } } diff --git a/app/src/main/java/com/hi/music/player/javabean/requestbody/BodyPlayUrl.java b/app/src/main/java/com/hi/music/player/javabean/requestbody/BodyPlayUrl.java new file mode 100644 index 0000000..03edf1f --- /dev/null +++ b/app/src/main/java/com/hi/music/player/javabean/requestbody/BodyPlayUrl.java @@ -0,0 +1,39 @@ +package com.hi.music.player.javabean.requestbody; + +import com.hi.music.player.javabean.requestbody.child.ContextBody; + +import java.io.Serializable; + + +/** + * 首页接口请求体 + */ +public class BodyPlayUrl implements Serializable { + + private String videoId ; + + private ContextBody context = new ContextBody(); + + + + + public String getVideoId() { + return videoId; + } + + public void setVideoId(String videoId) { + this.videoId = videoId; + } + + public ContextBody getContext() { + return context; + } + + public void setContext(ContextBody context) { + this.context = context; + } + + + + +} diff --git a/app/src/main/java/com/hi/music/player/javabean/requestbody/child/ContextBody.java b/app/src/main/java/com/hi/music/player/javabean/requestbody/child/ContextBody.java index 208a31e..a1c02c0 100644 --- a/app/src/main/java/com/hi/music/player/javabean/requestbody/child/ContextBody.java +++ b/app/src/main/java/com/hi/music/player/javabean/requestbody/child/ContextBody.java @@ -7,6 +7,13 @@ public class ContextBody { private Client client = new Client(); + private ThirdParty thirdParty = new ThirdParty(); + + + public ThirdParty getThirdParty() { + return thirdParty; + } + public Client getClient() { return client; } @@ -16,7 +23,7 @@ public class ContextBody { } - public class Client implements Serializable { + public static class Client implements Serializable { private String clientName = "WEB_REMIX"; private String clientVersion = "1.20220918"; private String hl = Locale.getDefault().getLanguage(); @@ -32,5 +39,27 @@ public class ContextBody { public void setVisitorData(String visitorData) { this.visitorData = visitorData; } + + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public void setClientVersion(String clientVersion) { + this.clientVersion = clientVersion; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + } + + public static class ThirdParty{ + //https://www.youtube.com/watch?v=UqyT8IEBkvY + private String embedUrl; + + public void setEmbedUrl(String embedUrl) { + this.embedUrl = embedUrl; + } } } diff --git a/app/src/main/java/com/hi/music/player/javabean/response/ResponseHome.java b/app/src/main/java/com/hi/music/player/javabean/response/ResponseHome.java index 329f59a..03bbd10 100644 --- a/app/src/main/java/com/hi/music/player/javabean/response/ResponseHome.java +++ b/app/src/main/java/com/hi/music/player/javabean/response/ResponseHome.java @@ -16,7 +16,7 @@ public class ResponseHome { private String continuation; @Nullable - //用于更多数据请求的visitorData(只有第一个接口会返回该值) + //用于更多数据请求的visitorData(只有第一个接口会返回该值,youtubei/v1/browse?prettyPrint=false) private String visitorData; @Nullable diff --git a/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlay.java b/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayListInfo.java similarity index 88% rename from app/src/main/java/com/hi/music/player/javabean/response/ResponsePlay.java rename to app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayListInfo.java index 1329845..91001ec 100644 --- a/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlay.java +++ b/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayListInfo.java @@ -1,6 +1,6 @@ package com.hi.music.player.javabean.response; -public class ResponsePlay { +public class ResponsePlayListInfo { //封面 private String covert; @@ -20,11 +20,22 @@ public class ResponsePlay { private String Duration; + //歌曲时长 毫秒 + private long DurationMs; + + private String videoId; private String playlistId; private String params; private String musicVideoType; + public long getDurationMs() { + return DurationMs; + } + + public void setDurationMs(long durationMs) { + DurationMs = durationMs; + } public String getDuration() { return Duration; diff --git a/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayUrl.java b/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayUrl.java new file mode 100644 index 0000000..b6abbb0 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/javabean/response/ResponsePlayUrl.java @@ -0,0 +1,55 @@ +package com.hi.music.player.javabean.response; + +public class ResponsePlayUrl { + + private String status; + private String audioUrlLow; + + private String audioUrlMedium; + private String videoId; + + public String getAudioUrlMedium() { + return audioUrlMedium; + } + + public void setAudioUrlMedium(String audioUrlMedium) { + this.audioUrlMedium = audioUrlMedium; + } + + + ///AUDIO_QUALITY_MEDIUM、AUDIO_QUALITY_LOW +// private String audioQuality; +// +// +// public String getAudioQuality() { +// return audioQuality; +// } +// +// public void setAudioQuality(String audioQuality) { +// this.audioQuality = audioQuality; +// } + + public String getVideoId() { + return videoId; + } + + public void setVideoId(String videoId) { + this.videoId = videoId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getAudioUrlLow() { + return audioUrlLow; + } + + public void setAudioUrlLow(String audioUrlLow) { + this.audioUrlLow = audioUrlLow; + } +} diff --git a/app/src/main/java/com/hi/music/player/media3/MyControllerView.java b/app/src/main/java/com/hi/music/player/media3/MyControllerView.java new file mode 100644 index 0000000..4ed458f --- /dev/null +++ b/app/src/main/java/com/hi/music/player/media3/MyControllerView.java @@ -0,0 +1,13 @@ +package com.hi.music.player.media3; + +import android.content.Context; + +import androidx.media3.common.util.UnstableApi; +import androidx.media3.ui.PlayerControlView; + +@UnstableApi +public class MyControllerView extends PlayerControlView{ + public MyControllerView(Context context) { + super(context); + } +} diff --git a/app/src/main/java/com/hi/music/player/media3/MyMediaControllerManager.java b/app/src/main/java/com/hi/music/player/media3/MyMediaControllerManager.java index 3aa5e9c..e4f0b15 100644 --- a/app/src/main/java/com/hi/music/player/media3/MyMediaControllerManager.java +++ b/app/src/main/java/com/hi/music/player/media3/MyMediaControllerManager.java @@ -2,16 +2,22 @@ package com.hi.music.player.media3; import android.content.ComponentName; +import androidx.annotation.OptIn; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.hi.music.player.MusicApplication; +import com.hi.music.player.api.MediaControllerListener; import com.hi.music.player.helper.CommonUtils; -import com.hi.music.player.javabean.response.ResponsePlay; +import com.hi.music.player.javabean.response.ResponsePlayListInfo; +import com.hi.music.player.javabean.response.ResponsePlayUrl; import com.hi.music.player.network.RetrofitManager; import java.util.ArrayList; @@ -34,7 +40,7 @@ public class MyMediaControllerManager { return myMediaControllerManagerInstance; } - public void init(){ + public void init(MediaControllerListener mediaControllerListener){ SessionToken sessionToken = new SessionToken(MusicApplication.myApplication, new ComponentName(MusicApplication.myApplication, PlaybackService.class)); ListenableFuture controllerFuture = @@ -46,8 +52,30 @@ public class MyMediaControllerManager { // playerView.setPlayer(controllerFuture.get()); try { mediaController = controllerFuture.get(); + mediaController.addListener(new Player.Listener() { + @Override + public void onPlayerError(PlaybackException error) { + CommonUtils.LogMsg("=-----PlaybackException+"+error.getMessage()); + } + + @Override + public void onPlaybackStateChanged(int playbackState) { + CommonUtils.LogMsg("=-----playbackState+"+playbackState); + mediaControllerListener.onPlayStatus(playbackState); + + } + @Override + public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) { + // 快进、快退等操作 + CommonUtils.LogMsg("=-----newPosition+"+newPosition.positionMs); +// mediaControllerListener.onPlayStatus(playbackState); + } + }); + mediaControllerListener.onMediaControllerComplete(true); + CommonUtils.LogMsg("=-----mediaController+"+mediaController); } catch (ExecutionException | InterruptedException e) { CommonUtils.LogErrorMsg(e.getMessage()); + mediaControllerListener.onMediaControllerComplete(false); } }, MoreExecutors.directExecutor()); @@ -55,17 +83,33 @@ public class MyMediaControllerManager { } - public void addMusicPlay(ResponsePlay responsePlay){ + public MediaController getMediaController() { + return mediaController; + } + + + public long getContentPos() { + return mediaController.getContentPosition(); + } + + @OptIn(markerClass = UnstableApi.class) + public void addMusicPlay(ResponsePlayListInfo playInfo, ResponsePlayUrl responsePlay){ List mediaItems = new ArrayList<>(); MediaItem.Builder builder = new MediaItem.Builder(); - builder.setUri(responsePlay.getVideoId()); + if(responsePlay.getAudioUrlMedium()!= null){ + builder.setUri(responsePlay.getAudioUrlMedium()); + }else { + builder.setUri(responsePlay.getAudioUrlLow()); + } + + builder.setMediaId(responsePlay.getVideoId()); MediaMetadata.Builder MediaMetadata_builder = new MediaMetadata.Builder(); -// MediaMetadata_builder.setMediaType(); -// MediaMetadata_builder.setAlbumTitle(); -// MediaMetadata_builder.setArtist(); -// MediaMetadata_builder.setDurationMs(); -// MediaMetadata_builder.setTitle(); -// MediaMetadata_builder.setRecordingYear(); + + MediaMetadata_builder.setArtist(playInfo.getSingerName()); +// MediaMetadata_builder.setDurationMs( ); + MediaMetadata_builder.setAlbumArtist(playInfo.getCovert()); + MediaMetadata_builder.setTitle(playInfo.getSongTitle()); +// MediaMetadata_builder.setRecordingYear(Integer.parseInt(playInfo.getYear())); builder.setMediaMetadata(MediaMetadata_builder.build()); mediaItems.add(builder.build()); @@ -76,4 +120,6 @@ public class MyMediaControllerManager { public void play(){ mediaController.play(); } + + } diff --git a/app/src/main/java/com/hi/music/player/media3/PlaybackService.java b/app/src/main/java/com/hi/music/player/media3/PlaybackService.java index a98d95f..32d17fa 100644 --- a/app/src/main/java/com/hi/music/player/media3/PlaybackService.java +++ b/app/src/main/java/com/hi/music/player/media3/PlaybackService.java @@ -1,27 +1,83 @@ package com.hi.music.player.media3; +import android.content.Intent; + + import androidx.annotation.Nullable; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; import androidx.media3.exoplayer.ExoPlayer; + +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.session.MediaSession; import androidx.media3.session.MediaSessionService; +import com.google.common.util.concurrent.ListenableFuture; +import com.hi.music.player.helper.CommonUtils; +import com.hi.music.player.javabean.response.ResponsePlayUrl; + +import java.util.ArrayList; +import java.util.List; + public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; + private ExoPlayer player; + @Override public void onCreate() { super.onCreate(); - ExoPlayer player = new ExoPlayer.Builder(this).build(); - mediaSession = new MediaSession.Builder(this, player).build(); + + AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.DEFAULT.usage) + .setContentType(AudioAttributes.DEFAULT.contentType) + .build(); + +// MediaSource.Factory mediaSourceFactory = +// new DefaultMediaSourceFactory(context) +// .setDataSourceFactory(cacheDataSourceFactory) +// .setLocalAdInsertionComponents(adsLoaderProvider); + + player = new ExoPlayer.Builder(this) + .setWakeMode(C.WAKE_MODE_LOCAL) +// .setMediaSourceFactory(mediaSourceFactory) + .setAudioAttributes(audioAttributes, true).build(); + + mediaSession = new MediaSession.Builder(this, player).setCallback(new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems(MediaSession mediaSession, MediaSession.ControllerInfo controller, List mediaItems) { + + CommonUtils.LogMsg("--------mediaItems="+mediaItems.get(0).mediaMetadata.title); +// player.addMediaItems(mediaItems); + + return MediaSession.Callback.super.onAddMediaItems(mediaSession, controller, mediaItems); + } + }) + .build(); + } + @Nullable @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } + @Override + public void onTaskRemoved(@Nullable Intent rootIntent) { + Player player = mediaSession.getPlayer(); +// if (player.getPlayWhenReady()) { +// // Make sure the service is not in foreground. +// player.pause(); +// } +// stopSelf(); + } + @Override public void onDestroy() { mediaSession.getPlayer().release(); diff --git a/app/src/main/java/com/hi/music/player/network/JsonHelper.java b/app/src/main/java/com/hi/music/player/network/JsonHelper.java index 9a0dc8f..8fed38a 100644 --- a/app/src/main/java/com/hi/music/player/network/JsonHelper.java +++ b/app/src/main/java/com/hi/music/player/network/JsonHelper.java @@ -1,7 +1,8 @@ package com.hi.music.player.network; import com.hi.music.player.helper.CommonUtils; -import com.hi.music.player.javabean.response.ResponsePlay; +import com.hi.music.player.javabean.response.ResponsePlayListInfo; +import com.hi.music.player.javabean.response.ResponsePlayUrl; import com.hi.music.player.javabean.response.child.ResponseCategory; import com.hi.music.player.javabean.response.ResponseHome; import com.hi.music.player.javabean.response.child.ResponseHomeChild; @@ -21,20 +22,11 @@ public class JsonHelper { public static ResponseHome ResolveHomeJson(JSONObject jsonObject) { ResponseHome responseHome = new ResponseHome(); try { - String bgUrl = getJsonUrl(jsonObject - .getJSONObject("background")); + String bgUrl = getJsonUrl(jsonObject.getJSONObject("background")); - JSONObject sectionListRenderer = jsonObject - .getJSONObject("contents") - .getJSONObject("singleColumnBrowseResultsRenderer") - .getJSONArray("tabs") - .getJSONObject(0) - .getJSONObject("tabRenderer") - .getJSONObject("content") - .getJSONObject("sectionListRenderer"); + JSONObject sectionListRenderer = jsonObject.getJSONObject("contents").getJSONObject("singleColumnBrowseResultsRenderer").getJSONArray("tabs").getJSONObject(0).getJSONObject("tabRenderer").getJSONObject("content").getJSONObject("sectionListRenderer"); - JSONObject responseContext = jsonObject - .getJSONObject("responseContext"); + JSONObject responseContext = jsonObject.getJSONObject("responseContext"); JSONArray serviceTrackingParams = responseContext.getJSONArray("serviceTrackingParams"); String visitorData = responseContext.getString("visitorData"); CommonUtils.LogMsg("---------参数-visitorData=" + visitorData); @@ -58,13 +50,10 @@ public class JsonHelper { ResponseHome responseHome = new ResponseHome(); try { - JSONObject sectionListContinuation = jsonObject - .getJSONObject("continuationContents") - .getJSONObject("sectionListContinuation"); + JSONObject sectionListContinuation = jsonObject.getJSONObject("continuationContents").getJSONObject("sectionListContinuation"); - JSONObject responseContext = jsonObject - .getJSONObject("responseContext"); + JSONObject responseContext = jsonObject.getJSONObject("responseContext"); JSONArray serviceTrackingParams = responseContext.getJSONArray("serviceTrackingParams"); getCommonHome(sectionListContinuation, responseHome); @@ -77,73 +66,10 @@ public class JsonHelper { return responseHome; } - - public static List ResolvePlayJson(JSONObject jsonObject) { - List list = new ArrayList<>(); - try { - JSONObject playlistPanelRenderer = jsonObject.getJSONObject("contents") - .getJSONObject("singleColumnMusicWatchNextResultsRenderer") - .getJSONObject("tabbedRenderer") - .getJSONObject("watchNextTabbedResultsRenderer") - .getJSONArray("tabs") - .getJSONObject(0) - .getJSONObject("tabRenderer") - .getJSONObject("content") - .getJSONObject("musicQueueRenderer") - .getJSONObject("content") - .getJSONObject("playlistPanelRenderer"); - - JSONArray contents = playlistPanelRenderer.getJSONArray("contents"); - - for (int i = 0; i < contents.length(); i++) { - ResponsePlay responsePlay = new ResponsePlay(); - JSONObject playlistPanelVideoRenderer = contents.getJSONObject(i) - .getJSONObject("playlistPanelVideoRenderer"); - - String jsonUrl = getJsonUrl(playlistPanelVideoRenderer); - String songName = getJsonTitle(playlistPanelVideoRenderer.getJSONObject("title"), 0); - - JSONObject longBylineText = playlistPanelVideoRenderer.getJSONObject("longBylineText"); - String singerName = getJsonTitle(longBylineText, 0); - String AlbumTitle = getJsonTitle(longBylineText, 2); - String YearRelease = getJsonTitle(longBylineText, 4); - - String SongDuration = getJsonTitle(playlistPanelVideoRenderer.getJSONObject("lengthText"), 0); - - String[] watchEndPoint = getWatchEndPoint(playlistPanelVideoRenderer); - - responsePlay.setCovert(jsonUrl); - responsePlay.setSongTitle(songName); - responsePlay.setSingerName(singerName); - responsePlay.setAlbumTitle(AlbumTitle); - responsePlay.setYear(YearRelease); - responsePlay.setDuration(SongDuration); - responsePlay.setVideoId(watchEndPoint[0]); - responsePlay.setPlaylistId(watchEndPoint[1]); - responsePlay.setParams(watchEndPoint[2]); - responsePlay.setMusicVideoType(watchEndPoint[3]); - - list.add(responsePlay); - CommonUtils.LogMsg("----------歌曲名字=" + songName + "---" + Arrays.toString(watchEndPoint)); - } - - - } catch (JSONException exception) { - CommonUtils.LogMsg("----------exception="); - exception.printStackTrace(); - - } - return list; - } - - private static void getCommonHome(JSONObject sectionListRenderer, ResponseHome responseHome) throws JSONException { - JSONObject nextContinuationData = sectionListRenderer - .getJSONArray("continuations") - .getJSONObject(0) - .getJSONObject("nextContinuationData"); + JSONObject nextContinuationData = sectionListRenderer.getJSONArray("continuations").getJSONObject(0).getJSONObject("nextContinuationData"); //token=continuation,cit= clickTrackingParams @@ -154,8 +80,7 @@ public class JsonHelper { responseHome.setClickTrackingParams(clickTrackingParams); responseHome.setContinuation(continuation); - JSONArray contents = sectionListRenderer - .getJSONArray("contents"); + JSONArray contents = sectionListRenderer.getJSONArray("contents"); List childList = new ArrayList<>(); @@ -167,14 +92,10 @@ public class JsonHelper { if (musicCarouselShelfRenderer != null) { //模块标题 - String title = getJsonTitle(musicCarouselShelfRenderer - .getJSONObject("header") - .getJSONObject("musicCarouselShelfBasicHeaderRenderer") - .getJSONObject("title"), 0); + String title = getJsonTitle(musicCarouselShelfRenderer.getJSONObject("header").getJSONObject("musicCarouselShelfBasicHeaderRenderer").getJSONObject("title"), 0); CommonUtils.LogMsg("----------headertitle=" + title); responseHomeChild.setHeaderTitle(title); - JSONArray childContents = musicCarouselShelfRenderer - .getJSONArray("contents"); + JSONArray childContents = musicCarouselShelfRenderer.getJSONArray("contents"); List categoryList = new ArrayList<>(); @@ -185,8 +106,7 @@ public class JsonHelper { if (musicResponsiveListItemRenderer != null) { ResponseSingle responseSingle = new ResponseSingle(); - String SingerHead = getJsonUrl(musicResponsiveListItemRenderer - .getJSONObject("thumbnail")); + String SingerHead = getJsonUrl(musicResponsiveListItemRenderer.getJSONObject("thumbnail")); JSONArray flexColumns = musicResponsiveListItemRenderer.getJSONArray("flexColumns"); @@ -196,16 +116,12 @@ public class JsonHelper { String SingerName = ""; String Description = ""; for (int g = 0; g < flexColumns.length(); g++) { - JSONObject jsonObject = musicResponsiveListItemRenderer.getJSONArray("flexColumns") - .getJSONObject(g) - .getJSONObject("musicResponsiveListItemFlexColumnRenderer") - .getJSONObject("text"); + JSONObject jsonObject = musicResponsiveListItemRenderer.getJSONArray("flexColumns").getJSONObject(g).getJSONObject("musicResponsiveListItemFlexColumnRenderer").getJSONObject("text"); String text = getJsonTitle(jsonObject, 0); if (g == 0) { SongTitle = text; - JSONObject runs = jsonObject.getJSONArray("runs") - .getJSONObject(0); + JSONObject runs = jsonObject.getJSONArray("runs").getJSONObject(0); String[] watchEndPoint = getWatchEndPoint(runs); @@ -229,8 +145,7 @@ public class JsonHelper { JSONObject musicTwoRowItemRenderer = jsonList.optJSONObject("musicTwoRowItemRenderer"); if (musicTwoRowItemRenderer != null) { ResponseCategory responseCategory = new ResponseCategory(); - String covert = getJsonUrl(musicTwoRowItemRenderer - .getJSONObject("thumbnailRenderer")); + String covert = getJsonUrl(musicTwoRowItemRenderer.getJSONObject("thumbnailRenderer")); JSONObject title1 = musicTwoRowItemRenderer.getJSONObject("title"); String twoTitle = getJsonTitle(title1, 0); @@ -259,14 +174,107 @@ public class JsonHelper { } + public static List ResolvePlayListJson(JSONObject jsonObject) { + List list = new ArrayList<>(); + try { + JSONObject playlistPanelRenderer = jsonObject.getJSONObject("contents").getJSONObject("singleColumnMusicWatchNextResultsRenderer").getJSONObject("tabbedRenderer").getJSONObject("watchNextTabbedResultsRenderer").getJSONArray("tabs").getJSONObject(0).getJSONObject("tabRenderer").getJSONObject("content").getJSONObject("musicQueueRenderer").getJSONObject("content").getJSONObject("playlistPanelRenderer"); + + JSONArray contents = playlistPanelRenderer.getJSONArray("contents"); + + for (int i = 0; i < contents.length(); i++) { + ResponsePlayListInfo responsePlayListInfo = new ResponsePlayListInfo(); + JSONObject playlistPanelVideoRenderer = contents.getJSONObject(i).getJSONObject("playlistPanelVideoRenderer"); + + String jsonUrl = getJsonUrl(playlistPanelVideoRenderer); + String songName = getJsonTitle(playlistPanelVideoRenderer.getJSONObject("title"), 0); + + JSONObject longBylineText = playlistPanelVideoRenderer.getJSONObject("longBylineText"); + String singerName = getJsonTitle(longBylineText, 0); + String AlbumTitle = getJsonTitle(longBylineText, 2); + String YearRelease = getJsonTitle(longBylineText, 4); + + String SongDuration = getJsonTitle(playlistPanelVideoRenderer.getJSONObject("lengthText"), 0); + + long ms = CommonUtils.convertToMilliseconds(SongDuration); + + + String[] watchEndPoint = getWatchEndPoint(playlistPanelVideoRenderer); + + responsePlayListInfo.setCovert(jsonUrl); + responsePlayListInfo.setSongTitle(songName); + responsePlayListInfo.setSingerName(singerName); + responsePlayListInfo.setAlbumTitle(AlbumTitle); + responsePlayListInfo.setYear(YearRelease); + responsePlayListInfo.setDuration(SongDuration); + responsePlayListInfo.setDurationMs(ms); + responsePlayListInfo.setVideoId(watchEndPoint[0]); + responsePlayListInfo.setPlaylistId(watchEndPoint[1]); + responsePlayListInfo.setParams(watchEndPoint[2]); + responsePlayListInfo.setMusicVideoType(watchEndPoint[3]); + + list.add(responsePlayListInfo); + CommonUtils.LogMsg("----------歌曲名字=" + songName + "---" + Arrays.toString(watchEndPoint)); + } + + + } catch (JSONException exception) { + CommonUtils.LogMsg("----------exception="); + exception.printStackTrace(); + + } + return list; + } + + public static ResponsePlayUrl ResolvePlayUrlJson(JSONObject jsonObject) { + try { + ResponsePlayUrl responsePlayUrl = new ResponsePlayUrl(); + String status = jsonObject.getJSONObject("playabilityStatus").getString("status"); + + JSONArray jsonArray = jsonObject.getJSONObject("streamingData").getJSONArray("adaptiveFormats"); + + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonIndex = jsonArray.getJSONObject(i); + + String mimeType = jsonIndex.getString("mimeType"); + String itag = jsonIndex.getString("itag"); + if (mimeType.contains("audio/mp4")) { + String url = jsonIndex.getString("url"); + boolean audioQuality1 = jsonIndex.has("audioQuality"); + CommonUtils.LogMsg("------------itag="+itag+"---------audioQuality1="+audioQuality1); + if (jsonIndex.has("audioQuality")) { + String audioQuality = jsonIndex.getString("audioQuality"); + if (audioQuality.equals("AUDIO_QUALITY_MEDIUM")) { + responsePlayUrl.setAudioUrlMedium(url); + } else { + responsePlayUrl.setAudioUrlLow(url); + } + } + } + + } + + + String videoId = jsonObject.getJSONObject("videoDetails").getString("videoId"); + + + responsePlayUrl.setVideoId(videoId); + + responsePlayUrl.setStatus(status); + return responsePlayUrl; + + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + + } + private static String[] getWatchEndPoint(JSONObject job) { String[] strings = new String[4]; try { - JSONObject watchEndpoint = job - .getJSONObject("navigationEndpoint") - .getJSONObject("watchEndpoint"); + JSONObject watchEndpoint = job.getJSONObject("navigationEndpoint").getJSONObject("watchEndpoint"); if (watchEndpoint.has("videoId")) { strings[0] = watchEndpoint.getString("videoId"); } @@ -277,9 +285,7 @@ public class JsonHelper { strings[2] = watchEndpoint.getString("params"); } if (watchEndpoint.has("watchEndpointMusicSupportedConfigs")) { - strings[3] = watchEndpoint.getJSONObject("watchEndpointMusicSupportedConfigs") - .getJSONObject("watchEndpointMusicConfig") - .getString("musicVideoType"); + strings[3] = watchEndpoint.getJSONObject("watchEndpointMusicSupportedConfigs").getJSONObject("watchEndpointMusicConfig").getString("musicVideoType"); } // String videoId = watchEndpoint.getString("videoId"); // String playlistId = watchEndpoint.getString("playlistId"); @@ -300,13 +306,10 @@ public class JsonHelper { if (b) { jsonObject = jsonObject.getJSONObject("musicThumbnailRenderer"); } - JSONArray jsonArray = jsonObject - .getJSONObject("thumbnail") - .getJSONArray("thumbnails"); + JSONArray jsonArray = jsonObject.getJSONObject("thumbnail").getJSONArray("thumbnails"); int length = jsonArray.length(); CommonUtils.LogMsg("----------length=" + (length - 1)); - String pngUrl = jsonArray.getJSONObject(length - 1) - .getString("url"); + String pngUrl = jsonArray.getJSONObject(length - 1).getString("url"); return pngUrl; } catch (JSONException exception) { @@ -320,9 +323,7 @@ public class JsonHelper { try { JSONArray runs = jsonObject.getJSONArray("runs"); if (index < runs.length()) { - text = runs - .getJSONObject(index) - .getString("text"); + text = runs.getJSONObject(index).getString("text"); } } catch (JSONException exception) { diff --git a/app/src/main/java/com/hi/music/player/network/MusicApi.java b/app/src/main/java/com/hi/music/player/network/MusicApi.java index cd09059..42951cb 100644 --- a/app/src/main/java/com/hi/music/player/network/MusicApi.java +++ b/app/src/main/java/com/hi/music/player/network/MusicApi.java @@ -4,6 +4,7 @@ import io.reactivex.Observable; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.http.Body; +import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.Headers; import retrofit2.http.POST; @@ -31,10 +32,25 @@ public interface MusicApi { + // X-Goog-FieldMask: contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer(continuations,contents(automixPreviewVideoRenderer,playlistPanelVideoRenderer(title,navigationEndpoint,longBylineText,shortBylineText,thumbnail,lengthText))) + + //获取播放列表 @POST("youtubei/v1/next?prettyPrint=false") @Headers("X-Goog-Api-Key:AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8") Observable getMusicPlayPage(@Header("X-Goog-FieldMask") String customHeader, @Body RequestBody requestBody); + + + @POST("youtubei/v1/player?prettyPrint=false") + @Headers({"X-Goog-Api-Key:AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", + "X-Goog-FieldMask:playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats,videoDetails.videoId"}) + Observable getMusicPlayUrl(@Body RequestBody requestBody); + + + + @GET("videoplayback?expire=1727100941&ei=rSPxZoSXJ_eM2_gPgqT8mAs&ip=146.19.167.8&id=o-ADGacaLEhb3NOOc74tfR50VCTKy0vnUb2_GAm-tPlv9n&itag=137&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&mh=f8&mm=31%2C29&mn=sn-tt1e7nlz%2Csn-vgqsknzd&ms=au%2Crdu&mv=m&mvi=2&pl=24&gcr=us&initcwndbps=6450000&vprv=1&svpuc=1&mime=video%2Fmp4&rqh=1&gir=yes&clen=4444925&dur=293.280&lmt=1688643558849956&mt=1727078942&fvip=3&keepalive=yes&fexp=51299152&c=ANDROID_MUSIC&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cgcr%2Cvprv%2Csvpuc%2Cmime%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&sig=AJfQdSswRgIhAKjjifMN7NMLqeoVXyqHPK1uHqev1PcnVMoycknt4QGfAiEAiCEcEYPDpQsCbE0tJ6MXjvPs4HmT0yM8Yoa26rWpc7M%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=ABPmVW0wRAIgeur5lMiKDgdWV5rrRTkmt0jbOQnifmVQwoTXk_Y17E0CIBfjGXpbdW2u3mtu1I-") + Observable getTest(); + } diff --git a/app/src/main/java/com/hi/music/player/network/RetrofitManager.java b/app/src/main/java/com/hi/music/player/network/RetrofitManager.java index d9514d4..21a2d52 100644 --- a/app/src/main/java/com/hi/music/player/network/RetrofitManager.java +++ b/app/src/main/java/com/hi/music/player/network/RetrofitManager.java @@ -2,12 +2,10 @@ package com.hi.music.player.network; import com.google.gson.Gson; import com.hi.music.player.api.RequestListener; -import com.hi.music.player.helper.CommonUtils; import com.hi.music.player.javabean.requestbody.BodyHome; import com.hi.music.player.javabean.requestbody.BodyPlay; -import com.hi.music.player.javabean.response.ResponseHome; - -import org.json.JSONObject; +import com.hi.music.player.javabean.requestbody.BodyPlayUrl; +import com.hi.music.player.javabean.requestbody.child.ContextBody; import java.util.HashMap; import java.util.concurrent.TimeUnit; @@ -26,6 +24,8 @@ import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitManager { private String base_Host = "https://music.youtube.com/"; + + private String base_Host_test = "https://rr2---sn-tt1e7nlz.googlevideo.com/"; private static volatile RetrofitManager REQUEST_MANAGER; private Retrofit retrofit; @@ -110,7 +110,7 @@ public class RetrofitManager { } - public void getNext(String params,String playlistId,String videoId,String musicVideoType,RequestListener requestListener) { + public void getPlayList(String params, String playlistId, String videoId, String musicVideoType, RequestListener requestListener) { BodyPlay bodyPlay = new BodyPlay(); bodyPlay.setParams(params); bodyPlay.setPlaylistId(playlistId); @@ -125,4 +125,29 @@ public class RetrofitManager { .observeOn(AndroidSchedulers.mainThread()) .subscribe(new ObserverWrapper(requestListener)); } + + + + public void getPlayUrl(String videoId,RequestListener requestListener) { + BodyPlayUrl bodyPlay = new BodyPlayUrl(); + bodyPlay.setVideoId(videoId); + ContextBody.Client client = bodyPlay.getContext().getClient(); + client.setClientName("ANDROID_MUSIC"); + client.setClientVersion("5.28.1"); + client.setPlatform("MOBILE"); + bodyPlay.getContext().getThirdParty().setEmbedUrl("https://www.youtube.com/watch?v="+videoId); + + + Gson gson = new Gson(); + String s = gson.toJson(bodyPlay); + RequestBody requestBody = RequestBody.Companion.create(s, JSON); + musicApi.getMusicPlayUrl(requestBody) + .subscribeOn(Schedulers.io()) + .unsubscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new ObserverWrapper(requestListener)); + } + + + } diff --git a/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java b/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java index b4c6408..20493b5 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/PlayActivity.java @@ -2,11 +2,17 @@ package com.hi.music.player.ui.activity; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.os.Handler; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.content.ContextCompat; import androidx.lifecycle.Observer; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaController; import com.bumptech.glide.Glide; import com.bumptech.glide.load.DataSource; @@ -17,10 +23,12 @@ import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; import com.hi.music.player.MusicApplication; import com.hi.music.player.R; +import com.hi.music.player.api.MediaControllerListener; import com.hi.music.player.databinding.ActivityPlayBinding; import com.hi.music.player.helper.CommonUtils; import com.hi.music.player.helper.MyValue; -import com.hi.music.player.javabean.response.ResponsePlay; +import com.hi.music.player.javabean.response.ResponsePlayListInfo; +import com.hi.music.player.javabean.response.ResponsePlayUrl; import com.hi.music.player.javabean.response.child.ResponseSingle; import com.hi.music.player.media3.MyMediaControllerManager; import com.hi.music.player.ui.activity.viewmodel.VMPlay; @@ -32,7 +40,13 @@ public class PlayActivity extends BaseActivity { private ResponseSingle responseSingle; private VMPlay vmPlay; - private MyMediaControllerManager mediaController; + private List mPlayList; + + private ResponsePlayUrl mCurPlayInfo; + private ResponsePlayListInfo musicInfo; + private Handler mHandler; + private Runnable mRunnable; + private MyMediaControllerManager mediaControllerManager; @Override protected ActivityPlayBinding getViewBinding() { @@ -44,21 +58,78 @@ public class PlayActivity extends BaseActivity { Intent intent = getIntent(); responseSingle = (ResponseSingle) intent.getSerializableExtra(MyValue.KEY_PLAY_ACTIVITY_SINGER); vmPlay = getActivityScopeViewModel(VMPlay.class); - vmPlay.getPlay(responseSingle); + vmPlay.getPlayUrl(responseSingle); + vmPlay.getPlayMusicList(responseSingle); + initPlayerView(); + initMediaController(); - vmPlay.data.observe(this, new Observer>() { + vmPlay.data.observe(this, new Observer>() { @Override - public void onChanged(List playList) { + public void onChanged(List playList) { if (playList.size() > 0) { - ResponsePlay responsePlay = playList.get(0); - loadCovert(responsePlay.getCovert()); - loadInfo(responsePlay); - + mPlayList = playList; + musicInfo = playList.get(0); + loadCovert(musicInfo.getCovert()); + loadInfo(musicInfo); + startPlayMusic(); } } }); + vmPlay.playUrlLiveData.observe(this, new Observer() { + @Override + public void onChanged(ResponsePlayUrl responsePlayUrl) { + mCurPlayInfo = responsePlayUrl; + CommonUtils.LogMsg("---------mCurPlayInfo=" + mCurPlayInfo.getAudioUrlLow()); + startPlayMusic(); + } + }); + + + } + + + @OptIn(markerClass = UnstableApi.class) + private void initPlayerView() { + vb.playerView.setShowRewindButton(false); + vb.playerView.setShowPreviousButton(false); + vb.playerView.setDefaultArtwork(ContextCompat.getDrawable(this, R.mipmap.ic_launcher)); + } + + private void initMediaController() { + mediaControllerManager = MyMediaControllerManager.getInstance(); + mediaControllerManager.init(new MediaControllerListener() { + + @Override + public void onMediaControllerComplete(boolean isOk) { + MediaController mediaController1 = mediaControllerManager.getMediaController(); + vb.playerView.setPlayer(mediaController1); + + startPlayMusic(); + } + + @Override + public void onPlayStatus(int playStatus) { + switch (playStatus) { + case Player.STATE_READY: + mHandler.post(mRunnable); + vb.progressBar.setVisibility(View.GONE); + break; + + case Player.STATE_ENDED: + mHandler.removeCallbacks(mRunnable); // 停止更新 + break; + + case Player.STATE_BUFFERING: + break; + + + } + + + } + }); } @Override @@ -66,14 +137,49 @@ public class PlayActivity extends BaseActivity { vb.btnPlay.setOnClickListener(this::onClick); } - private void loadInfo(ResponsePlay data) { + + /** + * 更新播放进度Ui + */ + private void updatePlaybackProgress() { + // 获取当前播放位置 + long contentPos = mediaControllerManager.getContentPos(); + long currentPosition = mediaControllerManager.getMediaController().getCurrentPosition(); + long duration = mediaControllerManager.getMediaController().getContentDuration(); + + long durationMs = musicInfo.getDurationMs(); + + String s = CommonUtils.convertMillisToTime(contentPos); + + + CommonUtils.LogMsg("---------播放进度-----contentPos=" + contentPos+"----currentPosition="+currentPosition+"------duration="+duration); + vb.tvCurrent.setText(s); + vb.seekbar.setValue(contentPos); + } + + + /** + * 初始化当前播放歌曲信息 + * @param data + */ + private void loadInfo(ResponsePlayListInfo data) { vb.tvSongName.setText(data.getSongTitle()); vb.tvSingerName.setText(data.getSingerName()); vb.tvDuration.setText(data.getDuration()); + vb.seekbar.setValueTo(data.getDurationMs()); + + } + + private void startPlayMusic() { + initProgressHandler(); + CommonUtils.LogMsg("00000"); + MediaController mediaController = mediaControllerManager.getMediaController(); + if (mCurPlayInfo != null && mediaController != null && mPlayList != null) { + mediaControllerManager.addMusicPlay(mPlayList.get(0), mCurPlayInfo); + mediaControllerManager.play(); + CommonUtils.LogMsg("111"); + } - mediaController = MyMediaControllerManager.getInstance(); - mediaController.init(); - mediaController.addMusicPlay(data); } private void loadCovert(String url) { @@ -89,8 +195,11 @@ public class PlayActivity extends BaseActivity { return false; } + @OptIn(markerClass = UnstableApi.class) @Override public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target target, @NonNull DataSource dataSource, boolean isFirstResource) { + vb.playerView.setDefaultArtwork(resource); + return false; } }) @@ -98,11 +207,16 @@ public class PlayActivity extends BaseActivity { } - - @Override - public void onStart() { - super.onStart(); - + private void initProgressHandler() { + mHandler = new Handler(); + mRunnable = new Runnable() { + @Override + public void run() { + updatePlaybackProgress(); + // 继续定时更新 + mHandler.postDelayed(this, 1000); // 每秒更新一次 + } + }; } @Override @@ -117,9 +231,16 @@ public class PlayActivity extends BaseActivity { @Override public void onClick(View v) { - if(v.equals(vb.btnPlay)){ - mediaController.play(); + if (v.equals(vb.btnPlay)) { + vb.btnPlay.setSelected(!vb.btnPlay.isSelected()); } } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mHandler != null && mRunnable != null) + mHandler.removeCallbacks(mRunnable); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMPlay.java b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMPlay.java index a44db91..6427c74 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMPlay.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/VMPlay.java @@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel; import com.hi.music.player.api.RequestListener; import com.hi.music.player.helper.CommonUtils; -import com.hi.music.player.javabean.response.ResponseHome; -import com.hi.music.player.javabean.response.ResponsePlay; +import com.hi.music.player.javabean.response.ResponsePlayListInfo; +import com.hi.music.player.javabean.response.ResponsePlayUrl; import com.hi.music.player.javabean.response.child.ResponseSingle; import com.hi.music.player.network.JsonHelper; import com.hi.music.player.network.RetrofitManager; @@ -21,18 +21,22 @@ import okhttp3.ResponseBody; public class VMPlay extends ViewModel { - private MutableLiveData> _data = new MutableLiveData>(); - public LiveData> data = _data; + private MutableLiveData> _data = new MutableLiveData>(); + public LiveData> data = _data; + + + private MutableLiveData _playUrlMutableLiveData = new MutableLiveData(); + public LiveData playUrlLiveData = _playUrlMutableLiveData; private String continuation, clickTrackingParams, visitorData; - public void getPlay(ResponseSingle responseSingle) { + public void getPlayMusicList(ResponseSingle responseSingle) { String playlistId = responseSingle.getPlaylistId(); String videoId = responseSingle.getVideoId(); String params = responseSingle.getParams(); String musicVideoType = responseSingle.getMusicVideoType(); - RetrofitManager.getInstance().getNext(params, playlistId, videoId, musicVideoType, new RequestListener() { + RetrofitManager.getInstance().getPlayList(params, playlistId, videoId, musicVideoType, new RequestListener() { @Override public void onFail(String errorMsg) { @@ -43,8 +47,8 @@ public class VMPlay extends ViewModel { public void onSuccess(ResponseBody data) { JSONObject jsonObject = CommonUtils.toJsonObject(data); if (jsonObject != null) { - List responsePlays = JsonHelper.ResolvePlayJson(jsonObject); - _data.setValue(responsePlays); + List responsePlayListInfos = JsonHelper.ResolvePlayListJson(jsonObject); + _data.setValue(responsePlayListInfos); } else { _data.setValue(null); } @@ -53,5 +57,28 @@ public class VMPlay extends ViewModel { }); } + public void getPlayUrl(ResponseSingle responseSingle) { + String videoId = responseSingle.getVideoId(); + RetrofitManager.getInstance().getPlayUrl(videoId, new RequestListener() { + + @Override + public void onFail(String errorMsg) { + _playUrlMutableLiveData.setValue(null); + } + + @Override + public void onSuccess(ResponseBody data) { + JSONObject jsonObject = CommonUtils.toJsonObject(data); + if (jsonObject != null) { + ResponsePlayUrl responsePlayUrl = JsonHelper.ResolvePlayUrlJson(jsonObject); + _playUrlMutableLiveData.setValue(responsePlayUrl); + } else { + _playUrlMutableLiveData.setValue(null); + } + + } + }); + } + } diff --git a/app/src/main/java/com/hi/music/player/ui/fragmnt/HomeFragment.java b/app/src/main/java/com/hi/music/player/ui/fragmnt/HomeFragment.java index b5e18e3..0ac1491 100644 --- a/app/src/main/java/com/hi/music/player/ui/fragmnt/HomeFragment.java +++ b/app/src/main/java/com/hi/music/player/ui/fragmnt/HomeFragment.java @@ -54,9 +54,11 @@ public class HomeFragment extends BaseFragment implements C vmHome.getHome(); + vmHome.data.observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(ResponseHome responseHome) { + if(responseHome == null)return; List childList1 = responseHome.getChildList(); childList.addAll(childList1); adapterHome.removeLoadingFooter(); diff --git a/app/src/main/res/layout/activity_play.xml b/app/src/main/res/layout/activity_play.xml index 3259cd2..c9d6947 100644 --- a/app/src/main/res/layout/activity_play.xml +++ b/app/src/main/res/layout/activity_play.xml @@ -31,6 +31,32 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/im_back" /> + + + + + + + - + + + \ No newline at end of file