From 91434dd2accd72d16547d9c3363d0d754ac46617 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 13 Feb 2017 00:55:05 +0100 Subject: [PATCH] setup core for search channel support --- .../org/schabi/newpipe/ChannelActivity.java | 5 +- .../newpipe/extractor/InfoItemCollector.java | 7 +- .../extractor/channel/ChannelInfoItem.java | 8 +- .../channel/ChannelInfoItemCollector.java | 61 ++++++++++ .../channel/ChannelInfoItemExtractor.java | 30 +++++ .../search/InfoItemSearchCollector.java | 11 +- .../extractor/search/SearchEngine.java | 7 +- .../extractor/search/SearchResult.java | 7 +- .../YoutubeChannelInfoItemExtractor.java | 49 ++++++++ .../services/youtube/YoutubeSearchEngine.java | 23 ++-- .../stream_info/StreamInfoItemCollector.java | 9 +- .../info_list/ChannelInfoItemHolder.java | 48 ++++++++ .../newpipe/info_list/InfoItemBuilder.java | 75 ++++++++++-- .../newpipe/info_list/InfoItemHolder.java | 27 +---- .../newpipe/info_list/InfoListAdapter.java | 41 ++++--- .../info_list/StreamInfoItemHolder.java | 58 ++++++++++ .../SearchInfoItemFragment.java | 14 ++- .../newpipe/search_fragment/SearchWorker.java | 20 +++- .../res/drawable-nodpi/buddy_channel_item.png | Bin 0 -> 3333 bytes .../dummi_thumbnail_playlist.png | Bin 0 -> 3147 bytes app/src/main/res/layout/channel_item.xml | 69 +++++++++++ app/src/main/res/layout/play_list_item.xml | 71 ++++++++++++ app/src/main/res/layout/stream_item.xml | 108 ++++++++++++++++++ assets/buddy_channel_item.svg | 85 ++++++++++++++ assets/dummi_thumbnail_playlist.svg | 105 +++++++++++++++++ .../dummi_thumbnail_playlist_background.svg | 72 ++++++++++++ 26 files changed, 930 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemCollector.java create mode 100644 app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemExtractor.java create mode 100644 app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelInfoItemExtractor.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java create mode 100644 app/src/main/res/drawable-nodpi/buddy_channel_item.png create mode 100644 app/src/main/res/drawable-nodpi/dummi_thumbnail_playlist.png create mode 100644 app/src/main/res/layout/channel_item.xml create mode 100644 app/src/main/res/layout/play_list_item.xml create mode 100644 app/src/main/res/layout/stream_item.xml create mode 100644 assets/buddy_channel_item.svg create mode 100644 assets/dummi_thumbnail_playlist.svg create mode 100644 assets/dummi_thumbnail_playlist_background.svg diff --git a/app/src/main/java/org/schabi/newpipe/ChannelActivity.java b/app/src/main/java/org/schabi/newpipe/ChannelActivity.java index e64605218..c2adea454 100644 --- a/app/src/main/java/org/schabi/newpipe/ChannelActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ChannelActivity.java @@ -92,7 +92,8 @@ public class ChannelActivity extends AppCompatActivity { final LinearLayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(infoListAdapter); - infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() { + infoListAdapter.setOnStreamItemSelectedListener( + new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(String url) { Intent detailIntent = new Intent(ChannelActivity.this, VideoItemDetailActivity.class); @@ -172,7 +173,7 @@ public class ChannelActivity extends AppCompatActivity { } private void addVideos(final ChannelInfo info) { - infoListAdapter.addStreamItemList(info.related_streams); + infoListAdapter.addInfoItemList(info.related_streams); } private void postNewErrorToast(Handler h, final int stringResource) { diff --git a/app/src/main/java/org/schabi/newpipe/extractor/InfoItemCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/InfoItemCollector.java index 1667e55dd..d3b0927a1 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/InfoItemCollector.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/InfoItemCollector.java @@ -28,11 +28,9 @@ import java.util.Vector; public class InfoItemCollector { private List itemList = new Vector<>(); private List errors = new Vector<>(); - private UrlIdHandler urlIdHandler; private int serviceId = -1; - public InfoItemCollector(UrlIdHandler handler, int serviceId) { - urlIdHandler = handler; + public InfoItemCollector(int serviceId) { this.serviceId = serviceId; } @@ -60,7 +58,4 @@ public class InfoItemCollector { protected int getServiceId() { return serviceId; } - protected UrlIdHandler getUrlIdHandler() { - return urlIdHandler; - } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItem.java b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItem.java index e598c659f..cb3f91a0f 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItem.java @@ -24,14 +24,18 @@ import org.schabi.newpipe.extractor.InfoItem; public class ChannelInfoItem implements InfoItem { + public int serviceId = -1; + public String channelName = ""; + public String webPageUrl = ""; + public int subscriberCount = -1; + public int videoAmount = -1; + public InfoType infoType() { return InfoType.CHANNEL; } - public String getTitle() { return ""; } - public String getLink() { return ""; } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemCollector.java new file mode 100644 index 000000000..cf18e81af --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemCollector.java @@ -0,0 +1,61 @@ +package org.schabi.newpipe.extractor.channel; + +import org.schabi.newpipe.extractor.InfoItemCollector; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.FoundAdException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import org.schabi.newpipe.extractor.stream_info.StreamInfoItemExtractor; + +/** + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2017 + * ChannelInfoItemCollector.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class ChannelInfoItemCollector extends InfoItemCollector { + public ChannelInfoItemCollector(int serviceId) { + super(serviceId); + } + + public void commit(ChannelInfoItemExtractor extractor) throws ParsingException { + try { + ChannelInfoItem resultItem = new ChannelInfoItem(); + // importand information + resultItem.channelName = extractor.getChannelName(); + + resultItem.serviceId = getServiceId(); + resultItem.webPageUrl = extractor.getWebPageUrl(); + + // optional information + try { + resultItem.subscriberCount = extractor.getSubscriberCount(); + } catch (Exception e) { + addError(e); + } + try { + resultItem.videoAmount = extractor.getVideoAmount(); + } catch (Exception e) { + addError(e); + } + + addItem(resultItem); + } catch (Exception e) { + addError(e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemExtractor.java new file mode 100644 index 000000000..12608d2ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfoItemExtractor.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.extractor.channel; + +import org.schabi.newpipe.extractor.exceptions.ParsingException; + +/** + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2017 + * ChannelInfoItemExtractor.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public interface ChannelInfoItemExtractor { + String getChannelName() throws ParsingException; + String getWebPageUrl() throws ParsingException; + int getSubscriberCount() throws ParsingException; + int getVideoAmount() throws ParsingException; +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/search/InfoItemSearchCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/search/InfoItemSearchCollector.java index 533331719..01d4f655f 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/search/InfoItemSearchCollector.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/search/InfoItemSearchCollector.java @@ -2,6 +2,8 @@ package org.schabi.newpipe.extractor.search; import org.schabi.newpipe.extractor.InfoItemCollector; import org.schabi.newpipe.extractor.UrlIdHandler; +import org.schabi.newpipe.extractor.channel.ChannelInfoItemCollector; +import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.stream_info.StreamInfoItemCollector; @@ -30,10 +32,12 @@ import org.schabi.newpipe.extractor.stream_info.StreamInfoItemExtractor; public class InfoItemSearchCollector extends InfoItemCollector { private String suggestion = ""; private StreamInfoItemCollector streamCollector; + private ChannelInfoItemCollector channelCollector; InfoItemSearchCollector(UrlIdHandler handler, int serviceId) { - super(handler, serviceId); + super(serviceId); streamCollector = new StreamInfoItemCollector(handler, serviceId); + channelCollector = new ChannelInfoItemCollector(serviceId); } public void setSuggestion(String suggestion) { @@ -43,6 +47,7 @@ public class InfoItemSearchCollector extends InfoItemCollector { public SearchResult getSearchResult() throws ExtractionException { SearchResult result = new SearchResult(); + addFromCollector(channelCollector); addFromCollector(streamCollector); result.suggestion = suggestion; @@ -54,4 +59,8 @@ public class InfoItemSearchCollector extends InfoItemCollector { public void commit(StreamInfoItemExtractor extractor) throws ParsingException { streamCollector.commit(extractor); } + + public void commit(ChannelInfoItemExtractor extractor) throws ParsingException { + channelCollector.commit(extractor); + } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java index ce7e6e061..76221528e 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchEngine.java @@ -5,6 +5,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream_info.StreamInfoItemCollector; import java.io.IOException; +import java.util.EnumSet; /** * Created by Christian Schabesberger on 10.08.15. @@ -27,6 +28,10 @@ import java.io.IOException; */ public abstract class SearchEngine { + public enum Filter { + VIDEO, CHANNEL, PLAY_LIST + } + public static class NothingFoundException extends ExtractionException { public NothingFoundException(String message) { super(message); @@ -43,6 +48,6 @@ public abstract class SearchEngine { } //Result search(String query, int page); public abstract InfoItemSearchCollector search( - String query, int page, String contentCountry) + String query, int page, String contentCountry, EnumSet filter) throws ExtractionException, IOException; } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchResult.java b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchResult.java index a3ef1fbcd..e81dc8803 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/search/SearchResult.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/search/SearchResult.java @@ -5,6 +5,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; import java.io.IOException; +import java.util.EnumSet; import java.util.List; import java.util.Vector; @@ -30,10 +31,12 @@ import java.util.Vector; public class SearchResult { public static SearchResult getSearchResult(SearchEngine engine, String query, - int page, String languageCode) + int page, String languageCode, EnumSet filter) throws ExtractionException, IOException { - SearchResult result = engine.search(query, page, languageCode).getSearchResult(); + SearchResult result = engine + .search(query, page, languageCode, filter) + .getSearchResult(); if(result.resultList.isEmpty()) { if(result.suggestion.isEmpty()) { throw new ExtractionException("Empty result despite no error"); diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelInfoItemExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelInfoItemExtractor.java new file mode 100644 index 000000000..51f31da6a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelInfoItemExtractor.java @@ -0,0 +1,49 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.jsoup.nodes.Element; + +/** + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2017 + * YoutubeChannelInfoItemExtractor.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor { + private Element el; + + public YoutubeChannelInfoItemExtractor(Element el) { + this.el = el; + } + + public String getChannelName() throws ParsingException { + return ""; + } + + public String getWebPageUrl() throws ParsingException { + return ""; + } + + public int getSubscriberCount() throws ParsingException { + return 0; + } + + public int getVideoAmount() throws ParsingException { + return 0; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java index 2b738204c..bcc94f01b 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java @@ -9,11 +9,10 @@ import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.search.InfoItemSearchCollector; import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItemCollector; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItemExtractor; import java.net.URLEncoder; import java.io.IOException; +import java.util.EnumSet; /** @@ -46,7 +45,10 @@ public class YoutubeSearchEngine extends SearchEngine { } @Override - public InfoItemSearchCollector search(String query, int page, String languageCode) + public InfoItemSearchCollector search(String query, + int page, + String languageCode, + EnumSet filter) throws IOException, ExtractionException { InfoItemSearchCollector collector = getInfoItemSearchCollector(); @@ -54,9 +56,13 @@ public class YoutubeSearchEngine extends SearchEngine { Downloader downloader = NewPipe.getDownloader(); String url = "https://www.youtube.com/results" - + "?search_query=" + URLEncoder.encode(query, CHARSET_UTF_8) - + "&page=" + Integer.toString(page + 1) - + "&filters=" + "video"; + + "?q=" + URLEncoder.encode(query, CHARSET_UTF_8) + + "&page=" + Integer.toString(page + 1); + if(filter.contains(Filter.VIDEO) && !filter.contains(Filter.CHANNEL)) { + url += "&sp=EgIQAQ%253D%253D"; + } else if(!filter.contains(Filter.VIDEO) && filter.contains(Filter.CHANNEL)) { + url += "&sp=EgIQAg%253D%253D"; + } String site; //String url = builder.build().toString(); @@ -94,12 +100,13 @@ public class YoutubeSearchEngine extends SearchEngine { } // search message item } else if ((el = item.select("div[class*=\"search-message\"]").first()) != null) { - //result.errorMessage = el.text(); throw new NothingFoundException(el.text()); // video item type - } else if ((el = item.select("div[class*=\"yt-lockup-video\"").first()) != null) { + } else if ((el = item.select("div[class*=\"yt-lockup-video\"]").first()) != null) { collector.commit(new YoutubeStreamInfoItemExtractor(el)); + } else if((el = item.select("div[class*=\"yt-lockup-channel\"]").first()) != null) { + collector.commit(new YoutubeChannelInfoItemExtractor(el)); } else { //noinspection ConstantConditions throw new ExtractionException("unexpected element found:\"" + el + "\""); diff --git a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfoItemCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfoItemCollector.java index 67db27e8a..1fc44df50 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfoItemCollector.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfoItemCollector.java @@ -31,8 +31,15 @@ import java.util.Vector; public class StreamInfoItemCollector extends InfoItemCollector { + private UrlIdHandler urlIdHandler; + public StreamInfoItemCollector(UrlIdHandler handler, int serviceId) { - super(handler, serviceId); + super(serviceId); + urlIdHandler = handler; + } + + private UrlIdHandler getUrlIdHandler() { + return urlIdHandler; } public void commit(StreamInfoItemExtractor extractor) throws ParsingException { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java new file mode 100644 index 000000000..50f4e3b24 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java @@ -0,0 +1,48 @@ +package org.schabi.newpipe.info_list; + +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; + +import de.hdodenhof.circleimageview.CircleImageView; + +/** + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * ChannelInfoItemHolder .java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class ChannelInfoItemHolder extends InfoItemHolder { + public final CircleImageView itemThumbnailView; + public final TextView itemChannelTitleView; + public final Button itemButton; + + ChannelInfoItemHolder(View v) { + super(v); + itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView); + itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView); + itemButton = (Button) v.findViewById(R.id.item_button); + } + + @Override + public InfoItem.InfoType infoType() { + return InfoItem.InfoType.CHANNEL; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index dd33d4caa..901f59fa6 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -5,6 +5,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; @@ -13,6 +14,7 @@ import org.schabi.newpipe.ImageErrorLoadingListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.AbstractStreamInfo; import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; /** @@ -37,7 +39,8 @@ import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; public class InfoItemBuilder { - public interface OnItemSelectedListener { + private static final String TAG = InfoItemBuilder.class.toString(); + public interface OnInfoItemSelectedListener { void selected(String url); } @@ -46,19 +49,64 @@ public class InfoItemBuilder { private ImageLoader imageLoader = ImageLoader.getInstance(); private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private OnItemSelectedListener onItemSelectedListener; + private OnInfoItemSelectedListener onStreamInfoItemSelectedListener; + private OnInfoItemSelectedListener onChannelInfoItemSelectedListener; public InfoItemBuilder(Activity a, View rootView) { activity = a; this.rootView = rootView; } - public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) { - this.onItemSelectedListener = onItemSelectedListener; + public void setOnStreamInfoItemSelectedListener( + OnInfoItemSelectedListener listener) { + this.onStreamInfoItemSelectedListener = listener; + } + + public void setOnChannelInfoItemSelectedListener( + OnInfoItemSelectedListener listener) { + this.onChannelInfoItemSelectedListener = listener; } public void buildByHolder(InfoItemHolder holder, final InfoItem i) { - final StreamInfoItem info = (StreamInfoItem) i; + switch(i.infoType()) { + case STREAM: + buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i); + break; + case CHANNEL: + buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i); + break; + case PLAYLIST: + Log.e(TAG, "Not yet implemented"); + break; + default: + Log.e(TAG, "Trollolo"); + } + } + + public View buildView(ViewGroup parent, final InfoItem info) { + View itemView = null; + InfoItemHolder holder = null; + switch(info.infoType()) { + case STREAM: + itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.stream_item, parent, false); + holder = new StreamInfoItemHolder(itemView); + break; + case CHANNEL: + itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.channel_item, parent, false); + holder = new ChannelInfoItemHolder(itemView); + break; + case PLAYLIST: + Log.e(TAG, "Not yet implemented"); + default: + Log.e(TAG, "Trollolo"); + } + buildByHolder(holder, info); + return itemView; + } + + private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) { if(info.infoType() != InfoItem.InfoType.STREAM) { Log.e("InfoItemBuilder", "Info type not yet supported"); } @@ -98,20 +146,23 @@ public class InfoItemBuilder { holder.itemButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onItemSelectedListener.selected(info.webpage_url); + onStreamInfoItemSelectedListener.selected(info.webpage_url); } }); } - public View buildView(ViewGroup parent, final InfoItem info) { - View streamPreviewView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.video_item, parent, false); - InfoItemHolder holder = new InfoItemHolder(streamPreviewView); - buildByHolder(holder, info); - return streamPreviewView; + private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) { + holder.itemChannelTitleView.setText(info.getTitle()); + holder.itemButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onChannelInfoItemSelectedListener.selected(info.getLink()); + } + }); } + public static String shortViewCount(Long viewCount){ if(viewCount >= 1000000000){ return Long.toString(viewCount/1000000000)+"B views"; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java index 690376465..c1fab069b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java @@ -2,14 +2,11 @@ package org.schabi.newpipe.info_list; import android.support.v7.widget.RecyclerView; import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; /** - * Created by Christian Schabesberger on 01.08.16. + * Created by Christian Schabesberger on 12.02.17. * * Copyright (C) Christian Schabesberger 2016 * InfoItemHolder.java is part of NewPipe. @@ -28,25 +25,9 @@ import org.schabi.newpipe.R; * along with NewPipe. If not, see . */ -public class InfoItemHolder extends RecyclerView.ViewHolder { - - public final ImageView itemThumbnailView; - public final TextView itemVideoTitleView, - itemUploaderView, - itemDurationView, - itemUploadDateView, - itemViewCountView; - public final Button itemButton; - +public abstract class InfoItemHolder extends RecyclerView.ViewHolder { public InfoItemHolder(View v) { super(v); - itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView); - itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); - itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView); - itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); - itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView); - itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView); - itemButton = (Button) v.findViewById(R.id.item_button); } - + public abstract InfoItem.InfoType infoType(); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 0daf211e5..5804fecd5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -2,13 +2,13 @@ package org.schabi.newpipe.info_list; import android.app.Activity; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; import java.util.List; import java.util.Vector; @@ -34,47 +34,58 @@ import java.util.Vector; */ public class InfoListAdapter extends RecyclerView.Adapter { + private static final String TAG = InfoListAdapter.class.toString(); private final InfoItemBuilder infoItemBuilder; - private final List streamList; + private final List infoItemList; public InfoListAdapter(Activity a, View rootView) { infoItemBuilder = new InfoItemBuilder(a, rootView); - streamList = new Vector<>(); + infoItemList = new Vector<>(); } - public void setOnItemSelectedListener - (InfoItemBuilder.OnItemSelectedListener onItemSelectedListener) { - infoItemBuilder.setOnItemSelectedListener(onItemSelectedListener); + public void setOnStreamItemSelectedListener + (InfoItemBuilder.OnInfoItemSelectedListener onItemSelectedListener) { + infoItemBuilder.setOnStreamInfoItemSelectedListener(onItemSelectedListener); } - public void addStreamItemList(List videos) { + public void addInfoItemList(List videos) { if(videos!= null) { - streamList.addAll(videos); + infoItemList.addAll(videos); notifyDataSetChanged(); } } public void clearSteamItemList() { - streamList.clear(); + infoItemList.clear(); notifyDataSetChanged(); } @Override public int getItemCount() { - return streamList.size(); + return infoItemList.size(); } @Override public InfoItemHolder onCreateViewHolder(ViewGroup parent, int i) { - View itemView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.video_item, parent, false); - - return new InfoItemHolder(itemView); + switch(infoItemList.get(i).infoType()) { + case STREAM: + return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.stream_item, parent, false)); + case CHANNEL: + return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.channel_item, parent, false)); + case PLAYLIST: + Log.e(TAG, "Playlist is not yet implemented"); + return null; + default: + Log.e(TAG, "Trollolo"); + return null; + } } @Override public void onBindViewHolder(InfoItemHolder holder, int i) { - infoItemBuilder.buildByHolder(holder, streamList.get(i)); + infoItemBuilder.buildByHolder(holder, infoItemList.get(i)); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java new file mode 100644 index 000000000..81981fd82 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.info_list; + +import android.icu.text.IDNA; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; + +/** + * Created by Christian Schabesberger on 01.08.16. + * + * Copyright (C) Christian Schabesberger 2016 + * StreamInfoItemHolder.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class StreamInfoItemHolder extends InfoItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView, + itemUploaderView, + itemDurationView, + itemUploadDateView, + itemViewCountView; + public final Button itemButton; + + public StreamInfoItemHolder(View v) { + super(v); + itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); + itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView); + itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); + itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView); + itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView); + itemButton = (Button) v.findViewById(R.id.item_button); + } + + @Override + public InfoItem.InfoType infoType() { + return InfoItem.InfoType.STREAM; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java index 09e7c0903..4e680d21c 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java @@ -21,6 +21,7 @@ import android.widget.Toast; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.report.ErrorActivity; @@ -29,6 +30,8 @@ import org.schabi.newpipe.detail.VideoItemDetailActivity; import org.schabi.newpipe.detail.VideoItemDetailFragment; import org.schabi.newpipe.info_list.InfoListAdapter; +import java.util.EnumSet; + import static android.app.Activity.RESULT_OK; import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; @@ -166,7 +169,7 @@ public class SearchInfoItemFragment extends Fragment { sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() { @Override public void onResult(SearchResult result) { - infoListAdapter.addStreamItemList(result.resultList); + infoListAdapter.addInfoItemList(result.resultList); setDoneLoading(); } @@ -213,7 +216,8 @@ public class SearchInfoItemFragment extends Fragment { infoListAdapter = new InfoListAdapter(getActivity(), getActivity().findViewById(android.R.id.content)); - infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() { + infoListAdapter.setOnStreamItemSelectedListener( + new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(String url) { startDetailActivity(url); @@ -298,7 +302,11 @@ public class SearchInfoItemFragment extends Fragment { private void search(String query, int page) { isLoading = true; SearchWorker sw = SearchWorker.getInstance(); - sw.search(streamingServiceId, query, page, getActivity()); + sw.search(streamingServiceId, + query, + page, + getActivity(), + EnumSet.of(SearchEngine.Filter.CHANNEL)); } private void setDoneLoading() { diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java index 9b1e8d86e..eafc0b6cf 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java @@ -16,6 +16,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import java.io.IOException; +import java.util.EnumSet; /** * Created by Christian Schabesberger on 02.08.16. @@ -67,14 +68,21 @@ public class SearchWorker { public static final String YOUTUBE = "Youtube"; private final String query; private final int page; + private final EnumSet filter; final Handler h = new Handler(); private volatile boolean runs = true; private Activity a = null; private int serviceId = -1; - public SearchRunnable(int serviceId, String query, int page, Activity activity, int requestId) { + public SearchRunnable(int serviceId, + String query, + int page, + EnumSet filter, + Activity activity, + int requestId) { this.serviceId = serviceId; this.query = query; this.page = page; + this.filter = filter; this.a = activity; } void terminate() { @@ -102,7 +110,7 @@ public class SearchWorker { String searchLanguage = sp.getString(searchLanguageKey, a.getString(R.string.default_language_value)); result = SearchResult - .getSearchResult(engine, query, page, searchLanguage); + .getSearchResult(engine, query, page, searchLanguage, filter); if(runs) { h.post(new ResultRunnable(result, requestId)); } @@ -180,11 +188,15 @@ public class SearchWorker { } - public void search(int serviceId, String query, int page, Activity a) { + public void search(int serviceId, + String query, + int page, + Activity a, + EnumSet filter) { if(runnable != null) { terminate(); } - runnable = new SearchRunnable(serviceId, query, page, a, requestId); + runnable = new SearchRunnable(serviceId, query, page, filter, a, requestId); Thread thread = new Thread(runnable); thread.start(); } diff --git a/app/src/main/res/drawable-nodpi/buddy_channel_item.png b/app/src/main/res/drawable-nodpi/buddy_channel_item.png new file mode 100644 index 0000000000000000000000000000000000000000..d43c2cbd996f25f7e9fe77a14a6ccabc8461e570 GIT binary patch literal 3333 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-nodpi/dummi_thumbnail_playlist.png b/app/src/main/res/drawable-nodpi/dummi_thumbnail_playlist.png new file mode 100644 index 0000000000000000000000000000000000000000..c70e4bf14d09b4cf7e63124a4408db9468eb4ab1 GIT binary patch literal 3147 zcmds3`#Y5B8=qt~Dl)RnM%m01mY6N2Er$~0kPsmhMnq%?%`i@@RBVH-&7ss(=8H1f zSxJu5I4nAB<~tbWFs7lxXa-G=P3%3{>-zri{SS7o>%HFRedl@a=e|Fm&-1yTOg9&M zmDL+oBM=A`M+aMX1Y$)STnMBRyfN(R`5A7Cu{MrJknl@F24}#tadUJos1(BVnPsPGTG!5F(UQ^Av(k)Dkij0VzB{%So74;cK?z1n_~W9 zZx4Izi$ZhMIqomdV{*M5M$=O3*xRsrPIiCXGw4KO1^c}h4)KmEILKAKPYv*0ZUA-{=r07gsoQA1?Lk5Gv_|CMB=37oGPc^OPH^w(R zX#TEUkde{V)ioNo$4BR|%g2rom-@7+^ecL~6x-_(lQF%ON4LT>;)YB*o&LJNpYb6h z=Bhrwo69}?VPayh?}fF>nAXp8Qzx$2U(5E|>?)tT@OJ0+Jw+B)R*Mp^{rLl<>Pr8>=DM_Zzy?c>AI!&3H5%=Fu;WPO&$hin< zpB!}K2f}DHGk|R(X_2DIN1X-!z~t5Ld%}WpLIUN5eh*G{{cX zU8ayLIi;h#atWxrD@wGnXMahEiP_FgHRs&UO5u!FN9r$2=&q>IR+em=5FM??^k>!0 zph9^2b85b(p3ZDYoHZel$#lVlw`GVP;5C56_&gOpGvZCm(!0iFBd8En*Sh=T5;?5s z6T5ZVnws|O=;#D%Npu~_km@sKm@j^2bT%eNoli7>0$9AXe(_U*zP>(peELbfT(iOr z9UKls`*UpU6whO;yNG!2D#+#L?FPKk`Tf9cJJ5wnv#) z{&I!O|4_r%q=6nmmfIxCryqTmj&M9((@ra62L20jJa+8Z5z7-nT3T9MF==>iu0~|| z>lQ-#mb3Bkqk1uOs=#3JMn{uhW+w#o+g3f%qXustnMG?IXA5%peE!^|@Bq)_`!&tT zKLc?Ya>#X4Wx}yr#ei*-`LIakQp=o_W6kPp>RgY16VTDoQ3TbLy526MDdei*-)QIO z=TFJzrMs=HX3RL9D16|9mX?;+0znS!gR;^_0Cn;JC=ha!$AB-av;4>6mll(G9)a)E zQ8js7qm9MM(L0=-&}o^n(es4_6!0*q=$;XY-x zb1qK7e3y>u@aU+4e?z=5S5@;OPN8}irFd8-YY-VmM>Hdy?Vv(`rWVivX!${A;Q%=h z_$R}gDYN;uRUIEF8e}Do`AsiagqpT;E%0=S;M z@$%%OesP%DhJRq%y2(04$Is8tWu-wj?&>!J3oZL{ugf5swzf7rB!Evx72M$Qc&~eV zGf!6fOl#EqNKhJ3c-Rbv;+c82{v5 z-^2!rqvO0)U|5}w%KMeU2O~_^_Vw|}+)I|&vIR`abK7HLCF}R);Sb&VJPua&QbRek zZ)craSc$Vky_KOwNNcwqLHN@~0K+1A;ZaRNex3IFLula*(KO|BWMnJdKBtVf{fayJ zfG0TW+eMAa$B#9EW62=PZ~9v1wL0xGEOkg#8C~VOEoJ^) ze*DL`j>~@Y^aA+J0IlKo&tvMfVxrjwF0nfK2c>~?xMuH77V&E3UW}UlK(Luw6y8B5 zlX5C*Ykih`ZVZ;HX3kbA5z)JWtgtHIGfRtKoXCxODDdczK&LN(gOH;{mKAg=ccG~P zEYux|IXKpFYcDsvN-optg*{T$u1BPm!^)V0rM;)|_55r+{V>#ER`^m*K7@r`V*{R# zWA^(vu^j5V`EGjPSS`Znk^r{Hx@5`5HVSJ~VPWAv@38WB^E=uSQj~;d=Lf^gfH>BIU%n zQ{7EFP0pGd8xgRH(-h0cj~@?l>Gqzk*E7~M-gSno+bj?5EPM$h^!m%I1JPmqioXs) zstzQOj%;C^Sy53ThZiI=XHLKxW#KUZ6o6e;dhErh!P&^jZLoEJ{Cf5 + + + + + + + + + + + + + + + + + + + + + + + + + + +