diff --git a/.gitignore b/.gitignore index bd009fd87..db7a554c0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,11 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties + +# vscode / eclipse files +*.classpath +*.project +*.settings +**/bin +**.vscode +*.code-workspace diff --git a/.travis.yml b/.travis.yml index f5bd81060..137cf3e20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ jdk: - openjdk8 script: - - ./gradlew check - - ./gradlew aggregatedJavadocs + - ./gradlew check aggregatedJavadocs deploy: provider: pages diff --git a/README.md b/README.md index 368886dfb..7e41be902 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ NewPipe Extractor is available at JitPack's Maven repo. If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps: 1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`. -2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.19.0'`the `dependencies` in your `build.gradle`. Replace `v0.19.0` with the latest release. +2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.19.5'`the `dependencies` in your `build.gradle`. Replace `v0.19.5` with the latest release. ### Testing changes diff --git a/build.gradle b/build.gradle index 6f349545f..9a2f92e14 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ allprojects { sourceCompatibility = 1.7 targetCompatibility = 1.7 - version 'v0.19.0' + version 'v0.19.5' group 'com.github.TeamNewPipe' repositories { diff --git a/extractor/build.gradle b/extractor/build.gradle index 2138df88e..205115870 100644 --- a/extractor/build.gradle +++ b/extractor/build.gradle @@ -2,10 +2,10 @@ dependencies { implementation project(':timeago-parser') implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'org.jsoup:jsoup:1.9.2' - implementation 'org.mozilla:rhino:1.7.7.1' - implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0' - implementation 'org.nibor.autolink:autolink:0.8.0' + implementation 'org.jsoup:jsoup:1.13.1' + implementation 'org.mozilla:rhino:1.7.12' + implementation 'com.github.spotbugs:spotbugs-annotations:4.0.2' + implementation 'org.nibor.autolink:autolink:0.10.0' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/ListExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/ListExtractor.java index 0e21f04b8..aae591eb6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/ListExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/ListExtractor.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; import java.util.Collections; @@ -9,6 +10,8 @@ import java.util.List; import javax.annotation.Nonnull; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + /** * Base class to extractors that have a list (e.g. playlists, users). */ @@ -63,8 +66,7 @@ public abstract class ListExtractor extends Extractor { public abstract InfoItemsPage getPage(final String pageUrl) throws IOException, ExtractionException; public boolean hasNextPage() throws IOException, ExtractionException { - final String nextPageUrl = getNextPageUrl(); - return nextPageUrl != null && !nextPageUrl.isEmpty(); + return !isNullOrEmpty(getNextPageUrl()); } @Override @@ -123,7 +125,7 @@ public abstract class ListExtractor extends Extractor { } public boolean hasNextPage() { - return nextPageUrl != null && !nextPageUrl.isEmpty(); + return !isNullOrEmpty(nextPageUrl); } public List getItems() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/ListInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/ListInfo.java index 1cb42e5da..38177e1e4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/ListInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/ListInfo.java @@ -4,6 +4,8 @@ import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import java.util.List; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + public abstract class ListInfo extends Info { private List relatedItems; private String nextPageUrl = null; @@ -37,7 +39,7 @@ public abstract class ListInfo extends Info { } public boolean hasNextPage() { - return nextPageUrl != null && !nextPageUrl.isEmpty(); + return !isNullOrEmpty(nextPageUrl); } public String getNextPageUrl() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelExtractor.java index 6d529d6b6..3f54f1c2c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelExtractor.java @@ -37,4 +37,7 @@ public abstract class ChannelExtractor extends ListExtractor { public abstract String getFeedUrl() throws ParsingException; public abstract long getSubscriberCount() throws ParsingException; public abstract String getDescription() throws ParsingException; + public abstract String getParentChannelName() throws ParsingException; + public abstract String getParentChannelUrl() throws ParsingException; + public abstract String getParentChannelAvatarUrl() throws ParsingException; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfo.java index 0fc44063b..52f18eadc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/channel/ChannelInfo.java @@ -94,16 +94,61 @@ public class ChannelInfo extends ListInfo { info.addError(e); } + try { + info.setParentChannelName(extractor.getParentChannelName()); + } catch (Exception e) { + info.addError(e); + } + + try { + info.setParentChannelUrl(extractor.getParentChannelUrl()); + } catch (Exception e) { + info.addError(e); + } + + try { + info.setParentChannelAvatarUrl(extractor.getParentChannelAvatarUrl()); + } catch (Exception e) { + info.addError(e); + } + return info; } private String avatarUrl; + private String parentChannelName; + private String parentChannelUrl; + private String parentChannelAvatarUrl; private String bannerUrl; private String feedUrl; private long subscriberCount = -1; private String description; private String[] donationLinks; + public String getParentChannelName() { + return parentChannelName; + } + + public void setParentChannelName(String parentChannelName) { + this.parentChannelName = parentChannelName; + } + + public String getParentChannelUrl() { + return parentChannelUrl; + } + + public void setParentChannelUrl(String parentChannelUrl) { + this.parentChannelUrl = parentChannelUrl; + } + + public String getParentChannelAvatarUrl() { + return parentChannelAvatarUrl; + } + + public void setParentChannelAvatarUrl(String parentChannelAvatarUrl) { + this.parentChannelAvatarUrl = parentChannelAvatarUrl; + } + public String getAvatarUrl() { return avatarUrl; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsExtractor.java index d1599affb..914e631dd 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsExtractor.java @@ -2,8 +2,11 @@ package org.schabi.newpipe.extractor.comments; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import javax.annotation.Nonnull; + public abstract class CommentsExtractor extends ListExtractor { public CommentsExtractor(StreamingService service, ListLinkHandler uiHandler) { @@ -11,4 +14,9 @@ public abstract class CommentsExtractor extends ListExtractor // TODO Auto-generated constructor stub } + @Nonnull + @Override + public String getName() throws ParsingException { + return "Comments"; + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItem.java index 6d6290716..e8871118a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItem.java @@ -9,11 +9,12 @@ public class CommentsInfoItem extends InfoItem { private String commentId; private String commentText; - private String authorName; - private String authorThumbnail; - private String authorEndpoint; - private String textualPublishedTime; - @Nullable private DateWrapper publishedTime; + private String uploaderName; + private String uploaderAvatarUrl; + private String uploaderUrl; + private String textualUploadDate; + @Nullable + private DateWrapper uploadDate; private int likeCount; public CommentsInfoItem(int serviceId, String url, String name) { @@ -36,45 +37,45 @@ public class CommentsInfoItem extends InfoItem { this.commentText = commentText; } - public String getAuthorName() { - return authorName; + public String getUploaderName() { + return uploaderName; } - public void setAuthorName(String authorName) { - this.authorName = authorName; + public void setUploaderName(String uploaderName) { + this.uploaderName = uploaderName; } - public String getAuthorThumbnail() { - return authorThumbnail; + public String getUploaderAvatarUrl() { + return uploaderAvatarUrl; } - public void setAuthorThumbnail(String authorThumbnail) { - this.authorThumbnail = authorThumbnail; + public void setUploaderAvatarUrl(String uploaderAvatarUrl) { + this.uploaderAvatarUrl = uploaderAvatarUrl; } - public String getAuthorEndpoint() { - return authorEndpoint; + public String getUploaderUrl() { + return uploaderUrl; } - public void setAuthorEndpoint(String authorEndpoint) { - this.authorEndpoint = authorEndpoint; + public void setUploaderUrl(String uploaderUrl) { + this.uploaderUrl = uploaderUrl; } - public String getTextualPublishedTime() { - return textualPublishedTime; + public String getTextualUploadDate() { + return textualUploadDate; } - public void setTextualPublishedTime(String textualPublishedTime) { - this.textualPublishedTime = textualPublishedTime; + public void setTextualUploadDate(String textualUploadDate) { + this.textualUploadDate = textualUploadDate; } @Nullable - public DateWrapper getPublishedTime() { - return publishedTime; + public DateWrapper getUploadDate() { + return uploadDate; } - public void setPublishedTime(@Nullable DateWrapper publishedTime) { - this.publishedTime = publishedTime; + public void setUploadDate(@Nullable DateWrapper uploadDate) { + this.uploadDate = uploadDate; } public int getLikeCount() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemExtractor.java index 7b40f8fa6..cf1501a22 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemExtractor.java @@ -3,17 +3,41 @@ package org.schabi.newpipe.extractor.comments; import org.schabi.newpipe.extractor.InfoItemExtractor; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.stream.StreamExtractor; import javax.annotation.Nullable; public interface CommentsInfoItemExtractor extends InfoItemExtractor { - String getCommentId() throws ParsingException; - String getCommentText() throws ParsingException; - String getAuthorName() throws ParsingException; - String getAuthorThumbnail() throws ParsingException; - String getAuthorEndpoint() throws ParsingException; - String getTextualPublishedTime() throws ParsingException; - @Nullable - DateWrapper getPublishedTime() throws ParsingException; + + /** + * Return the like count of the comment, or -1 if it's unavailable + * @see StreamExtractor#getLikeCount() + */ int getLikeCount() throws ParsingException; + + /** + * The text of the comment + */ + String getCommentText() throws ParsingException; + + /** + * The upload date given by the service, unmodified + * @see StreamExtractor#getTextualUploadDate() + */ + String getTextualUploadDate() throws ParsingException; + + /** + * The upload date wrapped with DateWrapper class + * @see StreamExtractor#getUploadDate() + */ + @Nullable + DateWrapper getUploadDate() throws ParsingException; + + String getCommentId() throws ParsingException; + + String getUploaderUrl() throws ParsingException; + + String getUploaderName() throws ParsingException; + + String getUploaderAvatarUrl() throws ParsingException; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemsCollector.java b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemsCollector.java index 030d11181..126d7b183 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemsCollector.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/comments/CommentsInfoItemsCollector.java @@ -35,27 +35,27 @@ public class CommentsInfoItemsCollector extends InfoItemsCollector { public PlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) { @@ -20,4 +22,9 @@ public abstract class PlaylistExtractor extends ListExtractor { public abstract String getUploaderAvatarUrl() throws ParsingException; public abstract long getStreamCount() throws ParsingException; + + @Nonnull public abstract String getSubChannelName() throws ParsingException; + @Nonnull public abstract String getSubChannelUrl() throws ParsingException; + @Nonnull public abstract String getSubChannelAvatarUrl() throws ParsingException; + } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/playlist/PlaylistInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/playlist/PlaylistInfo.java index f8c307380..089b1de55 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/playlist/PlaylistInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/playlist/PlaylistInfo.java @@ -84,6 +84,21 @@ public class PlaylistInfo extends ListInfo { info.setUploaderAvatarUrl(""); uploaderParsingErrors.add(e); } + try { + info.setSubChannelUrl(extractor.getSubChannelUrl()); + } catch (Exception e) { + uploaderParsingErrors.add(e); + } + try { + info.setSubChannelName(extractor.getSubChannelName()); + } catch (Exception e) { + uploaderParsingErrors.add(e); + } + try { + info.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl()); + } catch (Exception e) { + uploaderParsingErrors.add(e); + } try { info.setBannerUrl(extractor.getBannerUrl()); } catch (Exception e) { @@ -107,6 +122,9 @@ public class PlaylistInfo extends ListInfo { private String uploaderUrl; private String uploaderName; private String uploaderAvatarUrl; + private String subChannelUrl; + private String subChannelName; + private String subChannelAvatarUrl; private long streamCount = 0; public String getThumbnailUrl() { @@ -149,6 +167,30 @@ public class PlaylistInfo extends ListInfo { this.uploaderAvatarUrl = uploaderAvatarUrl; } + public String getSubChannelUrl() { + return subChannelUrl; + } + + public void setSubChannelUrl(String subChannelUrl) { + this.subChannelUrl = subChannelUrl; + } + + public String getSubChannelName() { + return subChannelName; + } + + public void setSubChannelName(String subChannelName) { + this.subChannelName = subChannelName; + } + + public String getSubChannelAvatarUrl() { + return subChannelAvatarUrl; + } + + public void setSubChannelAvatarUrl(String subChannelAvatarUrl) { + this.subChannelAvatarUrl = subChannelAvatarUrl; + } + public long getStreamCount() { return streamCount; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchExtractor.java index a7efea962..d1c481bb4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchExtractor.java @@ -25,6 +25,16 @@ public abstract class SearchExtractor extends ListExtractor { return getLinkHandler().getSearchString(); } + /** + * The search suggestion provided by the service. + *

+ * This method also returns the corrected query if + * {@link SearchExtractor#isCorrectedSearch()} is true. + * + * @return a suggestion to another query, the corrected query, or an empty String. + * @throws ParsingException + */ + @Nonnull public abstract String getSearchSuggestion() throws ParsingException; @Override @@ -37,4 +47,14 @@ public abstract class SearchExtractor extends ListExtractor { public String getName() { return getLinkHandler().getSearchString(); } + + /** + * Tell if the search was corrected by the service (if it's not exactly the search you typed). + *

+ * Example: on YouTube, if you search for "pewdeipie", + * it will give you results for "pewdiepie", then isCorrectedSearch should return true. + * + * @return whether the results comes from a corrected query or not. + */ + public abstract boolean isCorrectedSearch() throws ParsingException; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchInfo.java index eedce719e..b2e072cce 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/search/SearchInfo.java @@ -15,6 +15,7 @@ public class SearchInfo extends ListInfo { private String searchString; private String searchSuggestion; + private boolean isCorrectedSearch; public SearchInfo(int serviceId, SearchQueryHandler qIHandler, @@ -42,7 +43,12 @@ public class SearchInfo extends ListInfo { info.addError(e); } try { - info.searchSuggestion = extractor.getSearchSuggestion(); + info.setSearchSuggestion(extractor.getSearchSuggestion()); + } catch (Exception e) { + info.addError(e); + } + try { + info.setIsCorrectedSearch(extractor.isCorrectedSearch()); } catch (Exception e) { info.addError(e); } @@ -64,10 +70,22 @@ public class SearchInfo extends ListInfo { // Getter public String getSearchString() { - return searchString; + return this.searchString; } public String getSearchSuggestion() { - return searchSuggestion; + return this.searchSuggestion; + } + + public boolean isCorrectedSearch() { + return this.isCorrectedSearch; + } + + public void setIsCorrectedSearch(boolean isCorrectedSearch) { + this.isCorrectedSearch = isCorrectedSearch; + } + + public void setSearchSuggestion(String searchSuggestion) { + this.searchSuggestion = searchSuggestion; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCConferenceExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCConferenceExtractor.java index 4cd21c060..f11c13c22 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCConferenceExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCConferenceExtractor.java @@ -52,6 +52,21 @@ public class MediaCCCConferenceExtractor extends ChannelExtractor { return null; } + @Override + public String getParentChannelName() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelUrl() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelAvatarUrl() throws ParsingException { + return ""; + } + @Nonnull @Override public InfoItemsPage getInitialPage() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCSearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCSearchExtractor.java index d5ced534b..914c77497 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCSearchExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCSearchExtractor.java @@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler; import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector; import org.schabi.newpipe.extractor.search.SearchExtractor; @@ -42,9 +43,15 @@ public class MediaCCCSearchExtractor extends SearchExtractor { } } + @Nonnull @Override public String getSearchSuggestion() { - return null; + return ""; + } + + @Override + public boolean isCorrectedSearch() { + return false; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 894a0f0db..93772608e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -112,7 +112,25 @@ public class MediaCCCStreamExtractor extends StreamExtractor { @Nonnull @Override - public String getDashMpdUrl() { + public String getSubChannelUrl() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelName() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelAvatarUrl() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getDashMpdUrl() throws ParsingException { return ""; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeInstance.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeInstance.java index 82b675390..a29a592e0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeInstance.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeInstance.java @@ -3,13 +3,14 @@ package org.schabi.newpipe.extractor.services.peertube; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.utils.JsonUtils; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; @@ -43,7 +44,7 @@ public class PeertubeInstance { throw new Exception("unable to configure instance " + url, e); } - if (response == null || StringUtil.isBlank(response.responseBody())) { + if (response == null || Utils.isBlank(response.responseBody())) { throw new Exception("unable to configure instance " + url); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeParsingHelper.java index 7821aa297..f752ecfef 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeParsingHelper.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.extractor.services.peertube; import com.grack.nanojson.JsonObject; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.utils.Parser; +import org.schabi.newpipe.extractor.utils.Utils; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -13,12 +15,17 @@ import java.util.TimeZone; public class PeertubeParsingHelper { + public static final String START_KEY = "start"; + public static final String COUNT_KEY = "count"; + public static final int ITEMS_PER_PAGE = 12; + public static final String START_PATTERN = "start=(\\d*)"; + private PeertubeParsingHelper() { } public static void validate(JsonObject json) throws ContentNotAvailableException { String error = json.getString("error"); - if (!StringUtil.isBlank(error)) { + if (!Utils.isBlank(error)) { throw new ContentNotAvailableException(error); } } @@ -38,4 +45,26 @@ public class PeertubeParsingHelper { return uploadDate; } + public static String getNextPageUrl(String prevPageUrl, long total) { + String prevStart; + try { + prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); + } catch (Parser.RegexException e) { + return ""; + } + if (Utils.isBlank(prevStart)) return ""; + long nextStart = 0; + try { + nextStart = Long.parseLong(prevStart) + ITEMS_PER_PAGE; + } catch (NumberFormatException e) { + return ""; + } + + if (nextStart >= total) { + return ""; + } else { + return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + nextStart); + } + } + } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeService.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeService.java index e025c2be0..77deece79 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeService.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/PeertubeService.java @@ -44,8 +44,7 @@ public class PeertubeService extends StreamingService { @Override public ListLinkHandlerFactory getPlaylistLHFactory() { - // TODO Auto-generated method stub - return null; + return PeertubePlaylistLinkHandlerFactory.getInstance(); } @Override @@ -70,7 +69,6 @@ public class PeertubeService extends StreamingService { @Override public SubscriptionExtractor getSubscriptionExtractor() { - // TODO Auto-generated method stub return null; } @@ -88,8 +86,7 @@ public class PeertubeService extends StreamingService { @Override public PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler) throws ExtractionException { - // TODO Auto-generated method stub - return null; + return new PeertubePlaylistExtractor(this, linkHandler); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeAccountExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeAccountExtractor.java index 81cb0afae..7eabcc383 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeAccountExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeAccountExtractor.java @@ -4,7 +4,7 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelExtractor; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -16,17 +16,13 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.utils.JsonUtils; -import org.schabi.newpipe.extractor.utils.Parser; -import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; -public class PeertubeAccountExtractor extends ChannelExtractor { +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; - private static final String START_KEY = "start"; - private static final String COUNT_KEY = "count"; - private static final int ITEMS_PER_PAGE = 12; - private static final String START_PATTERN = "start=(\\d*)"; +public class PeertubeAccountExtractor extends ChannelExtractor { private InfoItemsPage initPage; private long total; @@ -75,6 +71,21 @@ public class PeertubeAccountExtractor extends ChannelExtractor { } } + @Override + public String getParentChannelName() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelUrl() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelAvatarUrl() throws ParsingException { + return ""; + } + @Override public InfoItemsPage getInitialPage() throws IOException, ExtractionException { super.fetchPage(); @@ -109,7 +120,7 @@ public class PeertubeAccountExtractor extends ChannelExtractor { public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { Response response = getDownloader().get(pageUrl); JsonObject json = null; - if (response != null && !StringUtil.isBlank(response.responseBody())) { + if (response != null && !Utils.isBlank(response.responseBody())) { try { json = JsonParser.object().from(response.responseBody()); } catch (Exception e) { @@ -120,36 +131,12 @@ public class PeertubeAccountExtractor extends ChannelExtractor { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); if (json != null) { PeertubeParsingHelper.validate(json); - Number number = JsonUtils.getNumber(json, "total"); - if (number != null) this.total = number.longValue(); + total = JsonUtils.getNumber(json, "total").longValue(); collectStreamsFrom(collector, json, pageUrl); } else { throw new ExtractionException("Unable to get PeerTube kiosk info"); } - return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl)); - } - - - private String getNextPageUrl(String prevPageUrl) { - String prevStart; - try { - prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); - } catch (RegexException e) { - return ""; - } - if (StringUtil.isBlank(prevStart)) return ""; - long nextStart = 0; - try { - nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE; - } catch (NumberFormatException e) { - return ""; - } - - if (nextStart >= total) { - return ""; - } else { - return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart)); - } + return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeChannelExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeChannelExtractor.java index dc27be80c..56d1caab5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeChannelExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeChannelExtractor.java @@ -4,7 +4,7 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelExtractor; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -18,15 +18,13 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; -public class PeertubeChannelExtractor extends ChannelExtractor { +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; - private static final String START_KEY = "start"; - private static final String COUNT_KEY = "count"; - private static final int ITEMS_PER_PAGE = 12; - private static final String START_PATTERN = "start=(\\d*)"; +public class PeertubeChannelExtractor extends ChannelExtractor { private InfoItemsPage initPage; private long total; @@ -75,6 +73,27 @@ public class PeertubeChannelExtractor extends ChannelExtractor { } } + @Override + public String getParentChannelName() throws ParsingException { + return JsonUtils.getString(json, "ownerAccount.name"); + } + + @Override + public String getParentChannelUrl() throws ParsingException { + return JsonUtils.getString(json, "ownerAccount.url"); + } + + @Override + public String getParentChannelAvatarUrl() throws ParsingException { + String value; + try { + value = JsonUtils.getString(json, "ownerAccount.avatar.path"); + } catch (Exception e) { + value = "/client/assets/images/default-avatar.png"; + } + return baseUrl + value; + } + @Override public InfoItemsPage getInitialPage() throws IOException, ExtractionException { super.fetchPage(); @@ -109,7 +128,7 @@ public class PeertubeChannelExtractor extends ChannelExtractor { public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { Response response = getDownloader().get(pageUrl); JsonObject json = null; - if (null != response && !StringUtil.isBlank(response.responseBody())) { + if (response != null && !Utils.isBlank(response.responseBody())) { try { json = JsonParser.object().from(response.responseBody()); } catch (Exception e) { @@ -120,36 +139,12 @@ public class PeertubeChannelExtractor extends ChannelExtractor { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); if (json != null) { PeertubeParsingHelper.validate(json); - Number number = JsonUtils.getNumber(json, "total"); - if (number != null) this.total = number.longValue(); + this.total = JsonUtils.getNumber(json, "total").longValue(); collectStreamsFrom(collector, json, pageUrl); } else { throw new ExtractionException("Unable to get PeerTube kiosk info"); } - return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl)); - } - - - private String getNextPageUrl(String prevPageUrl) { - String prevStart; - try { - prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); - } catch (RegexException e) { - return ""; - } - if (StringUtil.isBlank(prevStart)) return ""; - long nextStart = 0; - try { - nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE; - } catch (NumberFormatException e) { - return ""; - } - - if (nextStart >= total) { - return ""; - } else { - return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart)); - } + return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } @Override @@ -161,8 +156,7 @@ public class PeertubeChannelExtractor extends ChannelExtractor { throw new ExtractionException("Unable to extract PeerTube channel data"); } - String pageUrl = getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE; - this.initPage = getPage(pageUrl); + this.initPage = getPage(getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE); } private void setInitialData(String responseBody) throws ExtractionException { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsExtractor.java index 344774590..08f682cf9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsExtractor.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.comments.CommentsExtractor; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; @@ -13,18 +13,17 @@ import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; -public class PeertubeCommentsExtractor extends CommentsExtractor { +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; - private static final String START_KEY = "start"; - private static final String COUNT_KEY = "count"; - private static final int ITEMS_PER_PAGE = 12; - private static final String START_PATTERN = "start=(\\d*)"; +public class PeertubeCommentsExtractor extends CommentsExtractor { private InfoItemsPage initPage; private long total; @@ -33,11 +32,6 @@ public class PeertubeCommentsExtractor extends CommentsExtractor { super(service, uiHandler); } - @Override - public String getName() throws ParsingException { - return "Comments"; - } - @Override public InfoItemsPage getInitialPage() throws IOException, ExtractionException { super.fetchPage(); @@ -72,7 +66,7 @@ public class PeertubeCommentsExtractor extends CommentsExtractor { public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { Response response = getDownloader().get(pageUrl); JsonObject json = null; - if (null != response && !StringUtil.isBlank(response.responseBody())) { + if (response != null && !Utils.isBlank(response.responseBody())) { try { json = JsonParser.object().from(response.responseBody()); } catch (Exception e) { @@ -88,35 +82,11 @@ public class PeertubeCommentsExtractor extends CommentsExtractor { } else { throw new ExtractionException("Unable to get peertube comments info"); } - return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl)); + return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } @Override public void onFetchPage(Downloader downloader) throws IOException, ExtractionException { - String pageUrl = getUrl() + "?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE; - this.initPage = getPage(pageUrl); + this.initPage = getPage(getUrl() + "?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE); } - - private String getNextPageUrl(String prevPageUrl) { - String prevStart; - try { - prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); - } catch (RegexException e) { - return ""; - } - if (StringUtil.isBlank(prevStart)) return ""; - long nextStart = 0; - try { - nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE; - } catch (NumberFormatException e) { - return ""; - } - - if (nextStart >= total) { - return ""; - } else { - return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart)); - } - } - } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsInfoItemExtractor.java index d3b43fae2..e1dfa3241 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeCommentsInfoItemExtractor.java @@ -45,13 +45,13 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac } @Override - public String getTextualPublishedTime() throws ParsingException { + public String getTextualUploadDate() throws ParsingException { return JsonUtils.getString(item, "createdAt"); } @Override - public DateWrapper getPublishedTime() throws ParsingException { - String textualUploadDate = getTextualPublishedTime(); + public DateWrapper getUploadDate() throws ParsingException { + String textualUploadDate = getTextualUploadDate(); return new DateWrapper(PeertubeParsingHelper.parseDateFrom(textualUploadDate)); } @@ -78,7 +78,7 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac } @Override - public String getAuthorThumbnail() throws ParsingException { + public String getUploaderAvatarUrl() throws ParsingException { String value; try { value = JsonUtils.getString(item, "account.avatar.path"); @@ -89,12 +89,12 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac } @Override - public String getAuthorName() throws ParsingException { + public String getUploaderName() throws ParsingException { return JsonUtils.getString(item, "account.name") + "@" + JsonUtils.getString(item, "account.host"); } @Override - public String getAuthorEndpoint() throws ParsingException { + public String getUploaderUrl() throws ParsingException { String name = JsonUtils.getString(item, "account.name"); String host = JsonUtils.getString(item, "account.host"); return ServiceList.PeerTube.getChannelLHFactory().fromId("accounts/" + name + "@" + host, baseUrl).getUrl(); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubePlaylistExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubePlaylistExtractor.java index 1a96111ab..c689096ab 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubePlaylistExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubePlaylistExtractor.java @@ -1,85 +1,135 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; +import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistExtractor; +import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; +import org.schabi.newpipe.extractor.utils.JsonUtils; +import javax.annotation.Nonnull; import java.io.IOException; +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; + public class PeertubePlaylistExtractor extends PlaylistExtractor { + private JsonObject playlistInfo; + private JsonObject playlistVideos; + private String initialPageUrl; + + private long total; + public PeertubePlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) { super(service, linkHandler); - // TODO Auto-generated constructor stub } @Override public String getThumbnailUrl() throws ParsingException { - // TODO Auto-generated method stub - return null; + return getBaseUrl() + playlistInfo.getString("thumbnailPath"); } @Override public String getBannerUrl() throws ParsingException { - // TODO Auto-generated method stub return null; } @Override public String getUploaderUrl() throws ParsingException { - // TODO Auto-generated method stub - return null; + return playlistInfo.getObject("ownerAccount").getString("url"); } @Override public String getUploaderName() throws ParsingException { - // TODO Auto-generated method stub - return null; + return playlistInfo.getObject("ownerAccount").getString("displayName"); } @Override public String getUploaderAvatarUrl() throws ParsingException { - // TODO Auto-generated method stub - return null; + return getBaseUrl() + playlistInfo.getObject("ownerAccount").getObject("avatar").getString("path"); } @Override public long getStreamCount() throws ParsingException { - // TODO Auto-generated method stub - return 0; + return playlistInfo.getNumber("videosLength").longValue(); } + @Nonnull + @Override + public String getSubChannelName() throws ParsingException { + return playlistInfo.getObject("videoChannel").getString("displayName"); + } + + @Nonnull + @Override + public String getSubChannelUrl() throws ParsingException { + return playlistInfo.getObject("videoChannel").getString("url"); + } + + @Nonnull + @Override + public String getSubChannelAvatarUrl() throws ParsingException { + return getBaseUrl() + playlistInfo.getObject("videoChannel").getObject("avatar").getString("path"); + } + + @Nonnull @Override public InfoItemsPage getInitialPage() throws IOException, ExtractionException { - // TODO Auto-generated method stub - return null; + return getPage(initialPageUrl); } @Override public String getNextPageUrl() throws IOException, ExtractionException { - // TODO Auto-generated method stub - return null; + return PeertubeParsingHelper.getNextPageUrl(initialPageUrl, total); } @Override public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { - // TODO Auto-generated method stub - return null; + Response response = getDownloader().get(pageUrl); + try { + playlistVideos = JsonParser.object().from(response.responseBody()); + } catch (JsonParserException jpe) { + throw new ExtractionException("Could not parse json", jpe); + } + PeertubeParsingHelper.validate(playlistVideos); + + this.total = JsonUtils.getNumber(playlistVideos, "total").longValue(); + + StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); + + JsonArray videos = playlistVideos.getArray("data"); + for (Object o : videos) { + JsonObject video = ((JsonObject) o).getObject("video"); + collector.commit(new PeertubeStreamInfoItemExtractor(video, getBaseUrl())); + } + + return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } @Override - public void onFetchPage(Downloader downloader) throws IOException, ExtractionException { - + public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { + Response response = downloader.get(getUrl()); + try { + playlistInfo = JsonParser.object().from(response.responseBody()); + } catch (JsonParserException jpe) { + throw new ExtractionException("Could not parse json", jpe); + } + PeertubeParsingHelper.validate(playlistInfo); + initialPageUrl = getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE; } + @Nonnull @Override public String getName() throws ParsingException { - // TODO Auto-generated method stub - return null; + return playlistInfo.getString("displayName"); } - } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeSearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeSearchExtractor.java index 61fcdd6df..0c8d4fef1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeSearchExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeSearchExtractor.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItemExtractor; import org.schabi.newpipe.extractor.InfoItemsCollector; @@ -15,18 +15,18 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler; import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector; import org.schabi.newpipe.extractor.search.SearchExtractor; +import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; +import javax.annotation.Nonnull; import java.io.IOException; -public class PeertubeSearchExtractor extends SearchExtractor { +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; - private static final String START_KEY = "start"; - private static final String COUNT_KEY = "count"; - private static final int ITEMS_PER_PAGE = 12; - private static final String START_PATTERN = "start=(\\d*)"; +public class PeertubeSearchExtractor extends SearchExtractor { private InfoItemsPage initPage; private long total; @@ -35,9 +35,15 @@ public class PeertubeSearchExtractor extends SearchExtractor { super(service, linkHandler); } + @Nonnull @Override public String getSearchSuggestion() throws ParsingException { - return null; + return ""; + } + + @Override + public boolean isCorrectedSearch() { + return false; } @Override @@ -79,7 +85,7 @@ public class PeertubeSearchExtractor extends SearchExtractor { public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { Response response = getDownloader().get(pageUrl); JsonObject json = null; - if (null != response && !StringUtil.isBlank(response.responseBody())) { + if (null != response && !Utils.isBlank(response.responseBody())) { try { json = JsonParser.object().from(response.responseBody()); } catch (Exception e) { @@ -88,9 +94,8 @@ public class PeertubeSearchExtractor extends SearchExtractor { } if (json != null) { - Number number = JsonUtils.getNumber(json, "total"); - if (number != null) this.total = number.longValue(); - return new InfoItemsPage<>(collectStreamsFrom(json), getNextPageUrl(pageUrl)); + total = JsonUtils.getNumber(json, "total").longValue(); + return new InfoItemsPage<>(collectStreamsFrom(json), PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } else { throw new ExtractionException("Unable to get peertube search info"); } @@ -98,31 +103,6 @@ public class PeertubeSearchExtractor extends SearchExtractor { @Override public void onFetchPage(Downloader downloader) throws IOException, ExtractionException { - String pageUrl = getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE; - this.initPage = getPage(pageUrl); + initPage = getPage(getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE); } - - private String getNextPageUrl(String prevPageUrl) { - String prevStart; - try { - prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); - } catch (RegexException e) { - return ""; - } - if (StringUtil.isBlank(prevStart)) return ""; - long nextStart = 0; - try { - nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE; - } catch (NumberFormatException e) { - return ""; - } - - if (nextStart >= total) { - return ""; - } else { - return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart)); - } - } - - } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 3cf65154d..72b5bea93 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -4,7 +4,7 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -17,10 +17,18 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; -import org.schabi.newpipe.extractor.stream.*; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.StreamExtractor; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; +import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.JsonUtils; +import org.schabi.newpipe.extractor.utils.Utils; -import javax.annotation.Nonnull; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -29,6 +37,8 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import javax.annotation.Nonnull; + public class PeertubeStreamExtractor extends StreamExtractor { @@ -147,6 +157,29 @@ public class PeertubeStreamExtractor extends StreamExtractor { return baseUrl + value; } + @Override + public String getSubChannelUrl() throws ParsingException { + return JsonUtils.getString(json, "channel.url"); + } + + @Nonnull + @Override + public String getSubChannelName() throws ParsingException { + return JsonUtils.getString(json, "channel.displayName"); + } + + @Nonnull + @Override + public String getSubChannelAvatarUrl() throws ParsingException { + String value; + try { + value = JsonUtils.getString(json, "channel.avatar.path"); + } catch (Exception e) { + value = "/client/assets/images/default-avatar.png"; + } + return baseUrl + value; + } + @Override public String getDashMpdUrl() throws ParsingException { return ""; @@ -232,7 +265,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { } else { apiUrl = getUploaderUrl() + "/videos?start=0&count=8"; } - if (!StringUtil.isBlank(apiUrl)) getStreamsFromApi(collector, apiUrl); + if (!Utils.isBlank(apiUrl)) getStreamsFromApi(collector, apiUrl); return collector; } @@ -269,7 +302,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { private void getStreamsFromApi(StreamInfoItemsCollector collector, String apiUrl) throws ReCaptchaException, IOException, ParsingException { Response response = getDownloader().get(apiUrl); JsonObject relatedVideosJson = null; - if (null != response && !StringUtil.isBlank(response.responseBody())) { + if (null != response && !Utils.isBlank(response.responseBody())) { try { relatedVideosJson = JsonParser.object().from(response.responseBody()); } catch (JsonParserException e) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeTrendingExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeTrendingExtractor.java index e4bc1dff8..e6f0dc69d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeTrendingExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeTrendingExtractor.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; -import org.jsoup.helper.StringUtil; + import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Response; @@ -11,20 +11,17 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.kiosk.KioskExtractor; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.utils.JsonUtils; -import org.schabi.newpipe.extractor.utils.Parser; -import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; -public class PeertubeTrendingExtractor extends KioskExtractor { +import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*; - private static final String START_KEY = "start"; - private static final String COUNT_KEY = "count"; - private static final int ITEMS_PER_PAGE = 12; - private static final String START_PATTERN = "start=(\\d*)"; +public class PeertubeTrendingExtractor extends KioskExtractor { private InfoItemsPage initPage; private long total; @@ -73,7 +70,7 @@ public class PeertubeTrendingExtractor extends KioskExtractor { public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { Response response = getDownloader().get(pageUrl); JsonObject json = null; - if (null != response && !StringUtil.isBlank(response.responseBody())) { + if (response != null && !Utils.isBlank(response.responseBody())) { try { json = JsonParser.object().from(response.responseBody()); } catch (Exception e) { @@ -89,35 +86,12 @@ public class PeertubeTrendingExtractor extends KioskExtractor { } else { throw new ExtractionException("Unable to get peertube kiosk info"); } - return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl)); + return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total)); } @Override public void onFetchPage(Downloader downloader) throws IOException, ExtractionException { - String pageUrl = getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE; - this.initPage = getPage(pageUrl); - } - - private String getNextPageUrl(String prevPageUrl) { - String prevStart; - try { - prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl); - } catch (RegexException e) { - return ""; - } - if (StringUtil.isBlank(prevStart)) return ""; - long nextStart = 0; - try { - nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE; - } catch (NumberFormatException e) { - return ""; - } - - if (nextStart >= total) { - return ""; - } else { - return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart)); - } + this.initPage = getPage(getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubePlaylistLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubePlaylistLinkHandlerFactory.java index dd1bd77f5..72b1d87fd 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubePlaylistLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubePlaylistLinkHandlerFactory.java @@ -11,8 +11,7 @@ import java.util.List; public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory { private static final PeertubePlaylistLinkHandlerFactory instance = new PeertubePlaylistLinkHandlerFactory(); - private static final String ID_PATTERN = "/video-channels/([^/?&#]*)"; - private static final String VIDEO_CHANNELS_ENDPOINT = "/api/v1/video-channels/"; + private static final String ID_PATTERN = "/videos/watch/playlist/([^/?&#]*)"; public static PeertubePlaylistLinkHandlerFactory getInstance() { return instance; @@ -26,7 +25,7 @@ public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory { @Override public String getUrl(String id, List contentFilters, String sortFilter, String baseUrl) { - return baseUrl + VIDEO_CHANNELS_ENDPOINT + id; + return baseUrl + "/api/v1/video-playlists/" + id; } @Override @@ -34,9 +33,13 @@ public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory { return Parser.matchGroup1(ID_PATTERN, url); } - @Override public boolean onAcceptUrl(final String url) { - return url.contains("/video-channels/"); + try { + getId(url); + return true; + } catch (ParsingException e) { + return false; + } } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubeStreamLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubeStreamLinkHandlerFactory.java index 222dc1290..f181d3e71 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubeStreamLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/linkHandler/PeertubeStreamLinkHandlerFactory.java @@ -37,6 +37,12 @@ public class PeertubeStreamLinkHandlerFactory extends LinkHandlerFactory { @Override public boolean onAcceptUrl(final String url) throws FoundAdException { - return url.contains("/videos/"); + if (url.contains("/playlist/")) return false; + try { + getId(url); + return true; + } catch (ParsingException e) { + return false; + } } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudParsingHelper.java index 4ea615168..b8dafd834 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudParsingHelper.java @@ -15,9 +15,13 @@ import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelInfoItemExtractor; +import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStreamExtractor; +import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStreamInfoItemExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser.RegexException; +import org.schabi.newpipe.extractor.utils.Utils; import javax.annotation.Nonnull; import java.io.IOException; @@ -29,6 +33,7 @@ import java.util.*; import static java.util.Collections.singletonList; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps; public class SoundcloudParsingHelper { @@ -39,7 +44,7 @@ public class SoundcloudParsingHelper { } public static String clientId() throws ExtractionException, IOException { - if (clientId != null && !clientId.isEmpty()) return clientId; + if (!isNullOrEmpty(clientId)) return clientId; Downloader dl = NewPipe.getDownloader(); clientId = HARDCODED_CLIENT_ID; @@ -61,7 +66,7 @@ public class SoundcloudParsingHelper { for (Element element : possibleScripts) { final String srcUrl = element.attr("src"); - if (srcUrl != null && !srcUrl.isEmpty()) { + if (!isNullOrEmpty(srcUrl)) { try { return clientId = Parser.matchGroup1(clientIdPattern, dl.get(srcUrl, headers).responseBody()); } catch (RegexException ignored) { @@ -86,10 +91,12 @@ public class SoundcloudParsingHelper { } } - static Calendar parseDate(String textualUploadDate) throws ParsingException { + public static Calendar parseDateFrom(String textualUploadDate) throws ParsingException { Date date; try { - date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(textualUploadDate); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + date = sdf.parse(textualUploadDate); } catch (ParseException e1) { try { date = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss +0000").parse(textualUploadDate); @@ -256,13 +263,13 @@ public class SoundcloudParsingHelper { } @Nonnull - static String getUploaderUrl(JsonObject object) { + public static String getUploaderUrl(JsonObject object) { String url = object.getObject("user").getString("permalink_url", EMPTY_STRING); return replaceHttpWithHttps(url); } @Nonnull - static String getAvatarUrl(JsonObject object) { + public static String getAvatarUrl(JsonObject object) { String url = object.getObject("user").getString("avatar_url", EMPTY_STRING); return replaceHttpWithHttps(url); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudService.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudService.java index 1c2998791..e114d030b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudService.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudService.java @@ -10,18 +10,21 @@ import org.schabi.newpipe.extractor.linkhandler.*; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.playlist.PlaylistExtractor; import org.schabi.newpipe.extractor.search.SearchExtractor; +import org.schabi.newpipe.extractor.services.soundcloud.extractors.*; +import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.*; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import java.util.List; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO; +import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; public class SoundcloudService extends StreamingService { public SoundcloudService(int id) { - super(id, "SoundCloud", singletonList(AUDIO)); + super(id, "SoundCloud", asList(AUDIO, COMMENTS)); } @Override @@ -117,13 +120,13 @@ public class SoundcloudService extends StreamingService { @Override public ListLinkHandlerFactory getCommentsLHFactory() { - return null; + return SoundcloudCommentsLinkHandlerFactory.getInstance(); } @Override public CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler) throws ExtractionException { - return null; + return new SoundcloudCommentsExtractor(this, linkHandler); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelExtractor.java similarity index 87% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelExtractor.java index 3c51271c9..295824259 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -10,6 +10,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; @@ -17,6 +18,7 @@ import javax.annotation.Nonnull; import java.io.IOException; import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @SuppressWarnings("WeakerAccess") public class SoundcloudChannelExtractor extends ChannelExtractor { @@ -82,6 +84,21 @@ public class SoundcloudChannelExtractor extends ChannelExtractor { return user.getString("description", EMPTY_STRING); } + @Override + public String getParentChannelName() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelUrl() throws ParsingException { + return ""; + } + + @Override + public String getParentChannelAvatarUrl() throws ParsingException { + return ""; + } + @Nonnull @Override public InfoItemsPage getInitialPage() throws ExtractionException { @@ -116,7 +133,7 @@ public class SoundcloudChannelExtractor extends ChannelExtractor { @Override public InfoItemsPage getPage(final String pageUrl) throws IOException, ExtractionException { - if (pageUrl == null || pageUrl.isEmpty()) { + if (isNullOrEmpty(pageUrl)) { throw new ExtractionException(new IllegalArgumentException("Page url is empty or null")); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelInfoItemExtractor.java similarity index 95% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelInfoItemExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelInfoItemExtractor.java index 641438e79..274448588 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelInfoItemExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonObject; import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChartsExtractor.java similarity index 91% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChartsExtractor.java index 9a877faba..0c15d9c6b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChartsExtractor.java @@ -1,10 +1,11 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.kiosk.KioskExtractor; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; @@ -12,6 +13,7 @@ import javax.annotation.Nonnull; import java.io.IOException; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class SoundcloudChartsExtractor extends KioskExtractor { private StreamInfoItemsCollector collector = null; @@ -35,7 +37,7 @@ public class SoundcloudChartsExtractor extends KioskExtractor { @Override public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { - if (pageUrl == null || pageUrl.isEmpty()) { + if (isNullOrEmpty(pageUrl)) { throw new ExtractionException(new IllegalArgumentException("Page url is empty or null")); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsExtractor.java new file mode 100644 index 000000000..71c6469d8 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsExtractor.java @@ -0,0 +1,76 @@ +package org.schabi.newpipe.extractor.services.soundcloud.extractors; + +import com.grack.nanojson.JsonArray; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.comments.CommentsExtractor; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector; +import org.schabi.newpipe.extractor.downloader.Downloader; +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; + +import javax.annotation.Nonnull; +import java.io.IOException; + +public class SoundcloudCommentsExtractor extends CommentsExtractor { + + private JsonObject json; + + public SoundcloudCommentsExtractor(StreamingService service, ListLinkHandler uiHandler) { + super(service, uiHandler); + } + + @Nonnull + @Override + public InfoItemsPage getInitialPage() throws IOException, ExtractionException { + final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId()); + + collectStreamsFrom(collector, json.getArray("collection")); + + return new InfoItemsPage<>(collector, getNextPageUrl()); + } + + @Override + public String getNextPageUrl() throws IOException, ExtractionException { + return json.getString("next_href"); + } + + @Override + public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { + Downloader dl = NewPipe.getDownloader(); + Response rp = dl.get(pageUrl); + try { + json = JsonParser.object().from(rp.responseBody()); + } catch (JsonParserException e) { + throw new ParsingException("Could not parse json", e); + } + + final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId()); + collectStreamsFrom(collector, json.getArray("collection")); + + return new InfoItemsPage<>(collector, getNextPageUrl()); + } + + @Override + public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { + Response response = downloader.get(getUrl()); + try { + json = JsonParser.object().from(response.responseBody()); + } catch (JsonParserException e) { + throw new ParsingException("Could not parse json", e); + } + } + + private void collectStreamsFrom(final CommentsInfoItemsCollector collector, final JsonArray entries) throws ParsingException { + final String url = getUrl(); + for (Object comment : entries) { + collector.commit(new SoundcloudCommentsInfoItemExtractor((JsonObject) comment, url)); + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsInfoItemExtractor.java new file mode 100644 index 000000000..8a478dffd --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudCommentsInfoItemExtractor.java @@ -0,0 +1,76 @@ +package org.schabi.newpipe.extractor.services.soundcloud.extractors; + +import com.grack.nanojson.JsonObject; +import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; + +import javax.annotation.Nullable; + +public class SoundcloudCommentsInfoItemExtractor implements CommentsInfoItemExtractor { + + private JsonObject json; + private String url; + + public SoundcloudCommentsInfoItemExtractor(JsonObject json, String url) { + this.json = json; + this.url = url; + } + + @Override + public String getCommentId() throws ParsingException { + return json.getNumber("id").toString(); + } + + @Override + public String getCommentText() throws ParsingException { + return json.getString("body"); + } + + @Override + public String getUploaderName() throws ParsingException { + return json.getObject("user").getString("username"); + } + + @Override + public String getUploaderAvatarUrl() throws ParsingException { + return json.getObject("user").getString("avatar_url"); + } + + @Override + public String getUploaderUrl() throws ParsingException { + return json.getObject("user").getString("permalink_url"); + } + + @Override + public String getTextualUploadDate() throws ParsingException { + return json.getString("created_at"); + } + + @Nullable + @Override + public DateWrapper getUploadDate() throws ParsingException { + return new DateWrapper(SoundcloudParsingHelper.parseDateFrom(getTextualUploadDate())); + } + + @Override + public int getLikeCount() throws ParsingException { + return -1; + } + + @Override + public String getName() throws ParsingException { + return json.getObject("user").getString("permalink"); + } + + @Override + public String getUrl() throws ParsingException { + return url; + } + + @Override + public String getThumbnailUrl() throws ParsingException { + return json.getObject("user").getString("avatar_url"); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistExtractor.java similarity index 91% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistExtractor.java index 060b45caf..484c08fd4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistExtractor; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; @@ -20,6 +21,8 @@ import java.io.IOException; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + @SuppressWarnings("WeakerAccess") public class SoundcloudPlaylistExtractor extends PlaylistExtractor { private static final int streamsPerRequestedPage = 15; @@ -75,7 +78,7 @@ public class SoundcloudPlaylistExtractor extends PlaylistExtractor { for (StreamInfoItem item : infoItems.getItems()) { artworkUrl = item.getThumbnailUrl(); - if (artworkUrl != null && !artworkUrl.isEmpty()) break; + if (!isNullOrEmpty(artworkUrl)) break; } } catch (Exception ignored) { } @@ -113,6 +116,24 @@ public class SoundcloudPlaylistExtractor extends PlaylistExtractor { return playlist.getNumber("track_count", 0).longValue(); } + @Nonnull + @Override + public String getSubChannelName() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelUrl() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelAvatarUrl() throws ParsingException { + return ""; + } + @Nonnull @Override public InfoItemsPage getInitialPage() throws IOException, ExtractionException { @@ -159,7 +180,7 @@ public class SoundcloudPlaylistExtractor extends PlaylistExtractor { @Override public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException { - if (pageUrl == null || pageUrl.isEmpty()) { + if (isNullOrEmpty(pageUrl)) { throw new ExtractionException(new IllegalArgumentException("Page url is empty or null")); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistInfoItemExtractor.java similarity index 97% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistInfoItemExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistInfoItemExtractor.java index ae6875e73..f29efb1cc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudPlaylistInfoItemExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonObject; import org.schabi.newpipe.extractor.exceptions.ParsingException; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSearchExtractor.java similarity index 93% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSearchExtractor.java index 1e20818db..24f5987ec 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSearchExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -22,7 +22,7 @@ import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; -import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudSearchQueryHandlerFactory.ITEMS_PER_PAGE; +import static org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudSearchQueryHandlerFactory.ITEMS_PER_PAGE; import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; public class SoundcloudSearchExtractor extends SearchExtractor { @@ -33,9 +33,15 @@ public class SoundcloudSearchExtractor extends SearchExtractor { super(service, linkHandler); } + @Nonnull @Override public String getSearchSuggestion() { - return null; + return ""; + } + + @Override + public boolean isCorrectedSearch() { + return false; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java similarity index 93% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 1ee7ca334..431baff94 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -15,6 +15,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamExtractor; @@ -35,6 +36,7 @@ import java.util.Locale; import javax.annotation.Nonnull; import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class SoundcloudStreamExtractor extends StreamExtractor { private JsonObject track; @@ -74,7 +76,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { @Nonnull @Override public DateWrapper getUploadDate() throws ParsingException { - return new DateWrapper(SoundcloudParsingHelper.parseDate(track.getString("created_at"))); + return new DateWrapper(SoundcloudParsingHelper.parseDateFrom(track.getString("created_at"))); } @Nonnull @@ -141,6 +143,24 @@ public class SoundcloudStreamExtractor extends StreamExtractor { return SoundcloudParsingHelper.getAvatarUrl(track); } + @Nonnull + @Override + public String getSubChannelUrl() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelName() throws ParsingException { + return ""; + } + + @Nonnull + @Override + public String getSubChannelAvatarUrl() throws ParsingException { + return ""; + } + @Nonnull @Override public String getDashMpdUrl() { @@ -172,7 +192,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { JsonObject t = (JsonObject) transcoding; String url = t.getString("url"); - if (url != null && !url.isEmpty()) { + if (!isNullOrEmpty(url)) { // We can only play the mp3 format, but not handle m3u playlists / streams. // what about Opus? diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java similarity index 92% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamInfoItemExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java index 31a719ea4..3aef17ff7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java @@ -1,8 +1,9 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonObject; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; import org.schabi.newpipe.extractor.stream.StreamType; @@ -49,7 +50,7 @@ public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtracto @Override public DateWrapper getUploadDate() throws ParsingException { - return new DateWrapper(SoundcloudParsingHelper.parseDate(getTextualUploadDate())); + return new DateWrapper(SoundcloudParsingHelper.parseDateFrom(getTextualUploadDate())); } private String getCreatedAt() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSubscriptionExtractor.java similarity index 92% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSubscriptionExtractor.java index cf01b0c93..cabdb453e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSubscriptionExtractor.java @@ -1,8 +1,10 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudService; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSuggestionExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSuggestionExtractor.java similarity index 92% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSuggestionExtractor.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSuggestionExtractor.java index 924c46983..a204c9951 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSuggestionExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudSuggestionExtractor.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import java.io.IOException; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChannelLinkHandlerFactory.java similarity index 91% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelLinkHandlerFactory.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChannelLinkHandlerFactory.java index be1b9fcd3..4ef513b87 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChannelLinkHandlerFactory.java @@ -1,7 +1,8 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Utils; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChartsLinkHandlerFactory.java similarity index 93% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactory.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChartsLinkHandlerFactory.java index ef7e700f9..546564e4b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChartsLinkHandlerFactory.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; import org.schabi.newpipe.extractor.utils.Parser; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudCommentsLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudCommentsLinkHandlerFactory.java new file mode 100644 index 000000000..f899c9506 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudCommentsLinkHandlerFactory.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; + +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; + +import java.io.IOException; +import java.util.List; + +import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.clientId; + +public class SoundcloudCommentsLinkHandlerFactory extends ListLinkHandlerFactory { + + private static final SoundcloudCommentsLinkHandlerFactory instance = new SoundcloudCommentsLinkHandlerFactory(); + + public static SoundcloudCommentsLinkHandlerFactory getInstance() { + return instance; + } + + @Override + public String getUrl(String id, List contentFilter, String sortFilter) throws ParsingException { + try { + return "https://api-v2.soundcloud.com/tracks/" + id + "/comments" + "?client_id=" + clientId() + + "&threaded=0" + "&filter_replies=1"; // anything but 1 = sort by new + // + "&limit=NUMBER_OF_ITEMS_PER_REQUEST". We let the API control (default = 10) + // + "&offset=OFFSET". We let the API control (default = 0, then we use nextPageUrl) + } catch (ExtractionException | IOException e) { + throw new ParsingException("Could not get comments"); + } + } + + @Override + public String getId(String url) throws ParsingException { + // delagation to avoid duplicate code, as we need the same id + return SoundcloudStreamLinkHandlerFactory.getInstance().getId(url); + } + + @Override + public boolean onAcceptUrl(String url) { + try { + getId(url); + return true; + } catch (ParsingException e) { + return false; + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudPlaylistLinkHandlerFactory.java similarity index 91% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistLinkHandlerFactory.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudPlaylistLinkHandlerFactory.java index c7c882a93..53a70d3aa 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudPlaylistLinkHandlerFactory.java @@ -1,7 +1,8 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Utils; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchQueryHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudSearchQueryHandlerFactory.java similarity index 93% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchQueryHandlerFactory.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudSearchQueryHandlerFactory.java index dffcc91f0..ad19acda6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSearchQueryHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudSearchQueryHandlerFactory.java @@ -1,9 +1,10 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import java.io.IOException; import java.io.UnsupportedEncodingException; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java similarity index 91% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactory.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java index fc578411a..e271345d5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java @@ -1,7 +1,8 @@ -package org.schabi.newpipe.extractor.services.soundcloud; +package org.schabi.newpipe.extractor.services.soundcloud.linkHandler; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; +import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Utils; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java similarity index 95% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeParsingHelper.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 6ebb2497f..6df9b4b87 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -1,5 +1,4 @@ -package org.schabi.newpipe.extractor.services.youtube.linkHandler; - +package org.schabi.newpipe.extractor.services.youtube; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -28,8 +27,8 @@ import java.text.SimpleDateFormat; import java.util.*; import static org.schabi.newpipe.extractor.NewPipe.getDownloader; -import static org.schabi.newpipe.extractor.utils.Utils.HTTP; -import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; +import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.*; /* * Created by Christian Schabesberger on 02.03.16. @@ -111,9 +110,7 @@ public class YoutubeParsingHelper { public static long parseDurationString(String input) throws ParsingException, NumberFormatException { - // If time separator : is not detected, try . instead - final String[] splitInput = input.contains(":") ? input.split(":") : input.split("\\."); @@ -145,10 +142,10 @@ public class YoutubeParsingHelper { default: throw new ParsingException("Error duration string with unknown format: " + input); } - return ((((Long.parseLong(days) * 24) - + Long.parseLong(hours) * 60) - + Long.parseLong(minutes)) * 60) - + Long.parseLong(seconds); + return ((((Long.parseLong(Utils.removeNonDigitCharacters(days)) * 24) + + Long.parseLong(Utils.removeNonDigitCharacters(hours)) * 60) + + Long.parseLong(Utils.removeNonDigitCharacters(minutes))) * 60) + + Long.parseLong(Utils.removeNonDigitCharacters(seconds)); } public static String getFeedUrlFrom(final String channelIdOrUser) { @@ -201,7 +198,7 @@ public class YoutubeParsingHelper { * @throws ParsingException */ public static String getClientVersion() throws IOException, ExtractionException { - if (clientVersion != null && !clientVersion.isEmpty()) return clientVersion; + if (!isNullOrEmpty(clientVersion)) return clientVersion; if (isHardcodedClientVersionValid()) return clientVersion = HARDCODED_CLIENT_VERSION; final String url = "https://www.youtube.com/results?search_query=test"; @@ -244,7 +241,7 @@ public class YoutubeParsingHelper { for (String pattern : patterns) { try { contextClientVersion = Parser.matchGroup1(pattern, html); - if (contextClientVersion != null && !contextClientVersion.isEmpty()) { + if (!isNullOrEmpty(contextClientVersion)) { return clientVersion = contextClientVersion; } } catch (Exception ignored) { @@ -364,7 +361,7 @@ public class YoutubeParsingHelper { return "https://www.youtube.com/channel/" + browseId; } - if (canonicalBaseUrl != null && !canonicalBaseUrl.isEmpty()) { + if (!isNullOrEmpty(canonicalBaseUrl)) { return "https://www.youtube.com" + canonicalBaseUrl; } @@ -388,17 +385,21 @@ public class YoutubeParsingHelper { * Get the text from a JSON object that has either a simpleText or a runs array. * @param textObject JSON object to get the text from * @param html whether to return HTML, by parsing the navigationEndpoint - * @return text in the JSON object or an empty string + * @return text in the JSON object or {@code null} */ public static String getTextFromObject(JsonObject textObject, boolean html) throws ParsingException { + if (isNullOrEmpty(textObject)) return null; + if (textObject.has("simpleText")) return textObject.getString("simpleText"); + if (textObject.getArray("runs").isEmpty()) return null; + StringBuilder textBuilder = new StringBuilder(); for (Object textPart : textObject.getArray("runs")) { String text = ((JsonObject) textPart).getString("text"); if (html && ((JsonObject) textPart).has("navigationEndpoint")) { String url = getUrlFromNavigationEndpoint(((JsonObject) textPart).getObject("navigationEndpoint")); - if (url != null && !url.isEmpty()) { + if (!isNullOrEmpty(url)) { textBuilder.append("").append(text).append(""); continue; } @@ -490,12 +491,12 @@ public class YoutubeParsingHelper { * @param initialData the object which will be checked if an alert is present * @throws ContentNotAvailableException if an alert is detected */ - public static void defaultAlertsCheck(JsonObject initialData) throws ContentNotAvailableException { + public static void defaultAlertsCheck(final JsonObject initialData) throws ParsingException { final JsonArray alerts = initialData.getArray("alerts"); - if (!alerts.isEmpty()) { + if (!isNullOrEmpty(alerts)) { final JsonObject alertRenderer = alerts.getObject(0).getObject("alertRenderer"); - final String alertText = alertRenderer.getObject("text").getString("simpleText"); - final String alertType = alertRenderer.getString("type"); + final String alertText = getTextFromObject(alertRenderer.getObject("text")); + final String alertType = alertRenderer.getString("type", EMPTY_STRING); if (alertType.equalsIgnoreCase("ERROR")) { throw new ContentNotAvailableException("Got error: \"" + alertText + "\""); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java index 6a2a4b6c8..f5cfbc243 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java @@ -10,8 +10,8 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.localization.TimeAgoParser; +import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory; -import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.utils.Utils; @@ -19,8 +19,9 @@ import org.schabi.newpipe.extractor.utils.Utils; import javax.annotation.Nonnull; import java.io.IOException; -import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.*; -import static org.schabi.newpipe.extractor.utils.JsonUtils.*; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*; +import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; /* * Created by Christian Schabesberger on 25.07.16. @@ -49,7 +50,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor { /** * Some channels have response redirects and the only way to reliably get the id is by saving it. - *

+ *

* "Movies & Shows": *

      * UCuJcl0Ju-gPDoksRjK1ya-w ┐
@@ -130,7 +131,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
 
         if (!channelId.isEmpty()) {
             return channelId;
-        } else if (redirectedChannelId != null && !redirectedChannelId.isEmpty()) {
+        } else if (!isNullOrEmpty(redirectedChannelId)) {
             return redirectedChannelId;
         } else {
             throw new ParsingException("Could not get channel id");
@@ -163,7 +164,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
     public String getBannerUrl() throws ParsingException {
         try {
             String url = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("banner")
-                        .getArray("thumbnails").getObject(0).getString("url");
+                    .getArray("thumbnails").getObject(0).getString("url");
 
             if (url == null || url.contains("s.ytimg.com") || url.contains("default_banner")) {
                 return null;
@@ -212,6 +213,21 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
         }
     }
 
+    @Override
+    public String getParentChannelName() throws ParsingException {
+        return "";
+    }
+
+    @Override
+    public String getParentChannelUrl() throws ParsingException {
+        return "";
+    }
+
+    @Override
+    public String getParentChannelAvatarUrl() throws ParsingException {
+        return "";
+    }
+
     @Nonnull
     @Override
     public InfoItemsPage getInitialPage() throws ExtractionException {
@@ -229,7 +245,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
 
     @Override
     public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException {
-        if (pageUrl == null || pageUrl.isEmpty()) {
+        if (isNullOrEmpty(pageUrl)) {
             throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
         }
 
@@ -250,7 +266,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
 
 
     private String getNextPageUrlFrom(JsonArray continuations) {
-        if (continuations == null || continuations.isEmpty()) {
+        if (isNullOrEmpty(continuations)) {
             return "";
         }
 
@@ -306,10 +322,12 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
             throw new ContentNotSupportedException("This channel has no Videos tab");
         }
 
-        if (getTextFromObject(videoTab.getObject("content").getObject("sectionListRenderer")
-                .getArray("contents").getObject(0).getObject("itemSectionRenderer")
-                .getArray("contents").getObject(0).getObject("messageRenderer")
-                .getObject("text")).equals("This channel has no videos.")) {
+        final String messageRendererText = getTextFromObject(videoTab.getObject("content")
+                .getObject("sectionListRenderer").getArray("contents").getObject(0)
+                .getObject("itemSectionRenderer").getArray("contents").getObject(0)
+                .getObject("messageRenderer").getObject("text"));
+        if (messageRendererText != null
+                && messageRendererText.equals("This channel has no videos.")) {
             return null;
         }
 
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelInfoItemExtractor.java
index 68d1c48b7..881fbd794 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelInfoItemExtractor.java
@@ -7,8 +7,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
 import org.schabi.newpipe.extractor.utils.Utils;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.fixThumbnailUrl;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
 
 /*
  * Created by Christian Schabesberger on 12.02.17.
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java
index 78eb59f3d..1dea2952b 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.regex.Pattern;
 
 import static java.util.Collections.singletonList;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 
 public class YoutubeCommentsExtractor extends CommentsExtractor {
@@ -37,7 +38,6 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
 
     private String ytClientVersion;
     private String ytClientName;
-    private String title;
     private InfoItemsPage initPage;
 
     public YoutubeCommentsExtractor(StreamingService service, ListLinkHandler uiHandler) {
@@ -92,7 +92,7 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
 
     @Override
     public InfoItemsPage getPage(String pageUrl) throws IOException, ExtractionException {
-        if (pageUrl == null || pageUrl.isEmpty()) {
+        if (isNullOrEmpty(pageUrl)) {
             throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
         }
         String ajaxResponse = makeAjaxRequest(pageUrl);
@@ -116,7 +116,6 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
             //no comments
             return;
         }
-        fetchTitle(contents);
         List comments;
         try {
             comments = JsonUtils.getValues(contents, "commentThreadRenderer.comment.commentRenderer");
@@ -132,16 +131,6 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
         }
     }
 
-    private void fetchTitle(JsonArray contents) {
-        if (title == null) {
-            try {
-                title = getYoutubeText(JsonUtils.getObject(contents.getObject(0), "commentThreadRenderer.commentTargetTitle"));
-            } catch (Exception e) {
-                title = "Youtube Comments";
-            }
-        }
-    }
-
     @Override
     public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
         final Map> requestHeaders = new HashMap<>();
@@ -155,12 +144,6 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
         initPage = getPage(getNextPageUrl(commentsToken));
     }
 
-    @Nonnull
-    @Override
-    public String getName() throws ParsingException {
-        return title;
-    }
-
     private String makeAjaxRequest(String siteUrl) throws IOException, ReCaptchaException {
 
         Map> requestHeaders = new HashMap<>();
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsInfoItemExtractor.java
index 4757323ac..9d659f5d3 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsInfoItemExtractor.java
@@ -11,6 +11,8 @@ import org.schabi.newpipe.extractor.utils.Utils;
 
 import javax.annotation.Nullable;
 
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
 public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
 
     private final JsonObject json;
@@ -48,7 +50,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
     }
 
     @Override
-    public String getTextualPublishedTime() throws ParsingException {
+    public String getTextualUploadDate() throws ParsingException {
         try {
             return YoutubeCommentsExtractor.getYoutubeText(JsonUtils.getObject(json, "publishedTimeText"));
         } catch (Exception e) {
@@ -58,8 +60,8 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
 
     @Nullable
     @Override
-    public DateWrapper getPublishedTime() throws ParsingException {
-        String textualPublishedTime = getTextualPublishedTime();
+    public DateWrapper getUploadDate() throws ParsingException {
+        String textualPublishedTime = getTextualUploadDate();
         if (timeAgoParser != null && textualPublishedTime != null && !textualPublishedTime.isEmpty()) {
             return timeAgoParser.parse(textualPublishedTime);
         } else {
@@ -97,7 +99,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
     }
 
     @Override
-    public String getAuthorThumbnail() throws ParsingException {
+    public String getUploaderAvatarUrl() throws ParsingException {
         try {
             JsonArray arr = JsonUtils.getArray(json, "authorThumbnail.thumbnails");
             return JsonUtils.getString(arr.getObject(2), "url");
@@ -107,7 +109,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
     }
 
     @Override
-    public String getAuthorName() throws ParsingException {
+    public String getUploaderName() throws ParsingException {
         try {
             return YoutubeCommentsExtractor.getYoutubeText(JsonUtils.getObject(json, "authorText"));
         } catch (Exception e) {
@@ -116,7 +118,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
     }
 
     @Override
-    public String getAuthorEndpoint() throws ParsingException {
+    public String getUploaderUrl() throws ParsingException {
         try {
             return "https://youtube.com/channel/" + JsonUtils.getString(json, "authorEndpoint.browseEndpoint.browseId");
         } catch (Exception e) {
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedExtractor.java
index 6ed91fe4f..230915f72 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedExtractor.java
@@ -11,7 +11,7 @@ import org.schabi.newpipe.extractor.downloader.Response;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.feed.FeedExtractor;
 import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.stream.StreamInfoItem;
 import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
 
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMusicSearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMusicSearchExtractor.java
index 5f828a051..6d26e8d6a 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMusicSearchExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMusicSearchExtractor.java
@@ -5,6 +5,7 @@ import com.grack.nanojson.JsonObject;
 import com.grack.nanojson.JsonParser;
 import com.grack.nanojson.JsonParserException;
 import com.grack.nanojson.JsonWriter;
+
 import org.schabi.newpipe.extractor.InfoItem;
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -16,26 +17,26 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
 import org.schabi.newpipe.extractor.localization.TimeAgoParser;
 import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
 import org.schabi.newpipe.extractor.search.SearchExtractor;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.utils.JsonUtils;
 import org.schabi.newpipe.extractor.utils.Utils;
 
-import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.fixThumbnailUrl;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getValidJsonResponseBody;
+import javax.annotation.Nonnull;
+
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_ALBUMS;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_ARTISTS;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_PLAYLISTS;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_SONGS;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_VIDEOS;
 import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 public class YoutubeMusicSearchExtractor extends SearchExtractor {
     private JsonObject initialData;
@@ -125,15 +126,40 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
         return super.getUrl();
     }
 
+    @Nonnull
     @Override
     public String getSearchSuggestion() throws ParsingException {
-        final JsonObject didYouMeanRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
-                .getArray("contents").getObject(0).getObject("itemSectionRenderer")
-                .getArray("contents").getObject(0).getObject("didYouMeanRenderer");
-        if (!didYouMeanRenderer.has("correctedQuery")) {
+        final JsonObject itemSectionRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
+                .getArray("contents").getObject(0).getObject("itemSectionRenderer");
+        if (itemSectionRenderer.isEmpty()) {
             return "";
         }
-        return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
+
+        final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents")
+                .getObject(0).getObject("didYouMeanRenderer");
+        final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
+                .getObject("showingResultsForRenderer");
+
+        if (!didYouMeanRenderer.isEmpty()) {
+            return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
+        } else if (!showingResultsForRenderer.isEmpty()) {
+            return JsonUtils.getString(showingResultsForRenderer, "correctedQueryEndpoint.searchEndpoint.query");
+        } else {
+            return "";
+        }
+    }
+
+    @Override
+    public boolean isCorrectedSearch() {
+        final JsonObject itemSectionRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
+                .getArray("contents").getObject(0).getObject("itemSectionRenderer");
+        if (itemSectionRenderer.isEmpty()) {
+            return false;
+        }
+
+        final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
+                .getObject("showingResultsForRenderer");
+        return !showingResultsForRenderer.isEmpty();
     }
 
     @Nonnull
@@ -167,7 +193,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
 
     @Override
     public InfoItemsPage getPage(final String pageUrl) throws IOException, ExtractionException {
-        if (pageUrl == null || pageUrl.isEmpty()) {
+        if (isNullOrEmpty(pageUrl)) {
             throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
         }
 
@@ -239,7 +265,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         @Override
                         public String getUrl() throws ParsingException {
                             final String url = getUrlFromNavigationEndpoint(info.getObject("doubleTapCommand"));
-                            if (url != null && !url.isEmpty()) {
+                            if (!isNullOrEmpty(url)) {
                                 return url;
                             }
                             throw new ParsingException("Could not get url");
@@ -249,7 +275,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public String getName() throws ParsingException {
                             final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!name.isEmpty()) {
+                            if (!isNullOrEmpty(name)) {
                                 return name;
                             }
                             throw new ParsingException("Could not get name");
@@ -259,7 +285,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public long getDuration() throws ParsingException {
                             final String duration = getTextFromObject(info.getArray("flexColumns").getObject(3)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!duration.isEmpty()) {
+                            if (!isNullOrEmpty(duration)) {
                                 return YoutubeParsingHelper.parseDurationString(duration);
                             }
                             throw new ParsingException("Could not get duration");
@@ -269,7 +295,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public String getUploaderName() throws ParsingException {
                             final String name = getTextFromObject(info.getArray("flexColumns").getObject(1)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!name.isEmpty()) {
+                            if (!isNullOrEmpty(name)) {
                                 return name;
                             }
                             throw new ParsingException("Could not get uploader name");
@@ -288,13 +314,15 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
 
                                 return null;
                             } else {
-                                final JsonObject navigationEndpoint = info.getArray("flexColumns")
+                                final JsonObject navigationEndpointHolder = info.getArray("flexColumns")
                                         .getObject(1).getObject("musicResponsiveListItemFlexColumnRenderer")
-                                        .getObject("text").getArray("runs").getObject(0).getObject("navigationEndpoint");
+                                        .getObject("text").getArray("runs").getObject(0);
 
-                                final String url = getUrlFromNavigationEndpoint(navigationEndpoint);
+                                if (!navigationEndpointHolder.has("navigationEndpoint")) return null;
 
-                                if (url != null && !url.isEmpty()) {
+                                final String url = getUrlFromNavigationEndpoint(navigationEndpointHolder.getObject("navigationEndpoint"));
+
+                                if (!isNullOrEmpty(url)) {
                                     return url;
                                 }
 
@@ -319,7 +347,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                             }
                             final String viewCount = getTextFromObject(info.getArray("flexColumns").getObject(2)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!viewCount.isEmpty()) {
+                            if (!isNullOrEmpty(viewCount)) {
                                 return Utils.mixedNumberWordToLong(viewCount);
                             }
                             throw new ParsingException("Could not get view count");
@@ -359,7 +387,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public String getName() throws ParsingException {
                             final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!name.isEmpty()) {
+                            if (!isNullOrEmpty(name)) {
                                 return name;
                             }
                             throw new ParsingException("Could not get name");
@@ -368,7 +396,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         @Override
                         public String getUrl() throws ParsingException {
                             final String url = getUrlFromNavigationEndpoint(info.getObject("navigationEndpoint"));
-                            if (url != null && !url.isEmpty()) {
+                            if (!isNullOrEmpty(url)) {
                                 return url;
                             }
                             throw new ParsingException("Could not get url");
@@ -378,7 +406,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public long getSubscriberCount() throws ParsingException {
                             final String viewCount = getTextFromObject(info.getArray("flexColumns").getObject(2)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!viewCount.isEmpty()) {
+                            if (!isNullOrEmpty(viewCount)) {
                                 return Utils.mixedNumberWordToLong(viewCount);
                             }
                             throw new ParsingException("Could not get subscriber count");
@@ -414,7 +442,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         public String getName() throws ParsingException {
                             final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!name.isEmpty()) {
+                            if (!isNullOrEmpty(name)) {
                                 return name;
                             }
                             throw new ParsingException("Could not get name");
@@ -423,7 +451,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                         @Override
                         public String getUrl() throws ParsingException {
                             final String url = getUrlFromNavigationEndpoint(info.getObject("doubleTapCommand"));
-                            if (url != null && !url.isEmpty()) {
+                            if (!isNullOrEmpty(url)) {
                                 return url;
                             }
                             throw new ParsingException("Could not get url");
@@ -439,7 +467,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                                 name = getTextFromObject(info.getArray("flexColumns").getObject(1)
                                         .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
                             }
-                            if (!name.isEmpty()) {
+                            if (!isNullOrEmpty(name)) {
                                 return name;
                             }
                             throw new ParsingException("Could not get uploader name");
@@ -452,7 +480,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
                             }
                             final String count = getTextFromObject(info.getArray("flexColumns").getObject(2)
                                     .getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
-                            if (!count.isEmpty()) {
+                            if (!isNullOrEmpty(count)) {
                                 if (count.contains("100+")) {
                                     return ITEM_COUNT_MORE_THAN_100;
                                 } else {
@@ -468,7 +496,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
     }
 
     private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException, IOException, ReCaptchaException {
-        if (continuations == null || continuations.isEmpty()) {
+        if (isNullOrEmpty(continuations)) {
             return "";
         }
 
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistExtractor.java
index 4f1884193..12d6d49e6 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistExtractor.java
@@ -2,28 +2,37 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
 
 import com.grack.nanojson.JsonArray;
 import com.grack.nanojson.JsonObject;
+
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.downloader.Downloader;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
+import org.schabi.newpipe.extractor.localization.DateWrapper;
 import org.schabi.newpipe.extractor.localization.TimeAgoParser;
 import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
 import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
 import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
+import org.schabi.newpipe.extractor.stream.StreamType;
 import org.schabi.newpipe.extractor.utils.Utils;
 
-import javax.annotation.Nonnull;
 import java.io.IOException;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.fixThumbnailUrl;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 @SuppressWarnings("WeakerAccess")
 public class YoutubePlaylistExtractor extends PlaylistExtractor {
+    private JsonArray initialAjaxJson;
     private JsonObject initialData;
     private JsonObject playlistInfo;
 
@@ -35,9 +44,9 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
     public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
         final String url = getUrl() + "&pbj=1";
 
-        final JsonArray ajaxJson = getJsonResponse(url, getExtractorLocalization());
+        initialAjaxJson = getJsonResponse(url, getExtractorLocalization());
 
-        initialData = ajaxJson.getObject(1).getObject("response");
+        initialData = initialAjaxJson.getObject(1).getObject("response");
         YoutubeParsingHelper.defaultAlertsCheck(initialData);
 
         playlistInfo = getPlaylistInfo();
@@ -81,7 +90,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
     @Override
     public String getName() throws ParsingException {
         String name = getTextFromObject(playlistInfo.getObject("title"));
-        if (!name.isEmpty()) return name;
+        if (name != null && !name.isEmpty()) return name;
 
         return initialData.getObject("microformat").getObject("microformatDataRenderer").getString("title");
     }
@@ -91,11 +100,11 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
         String url = playlistInfo.getObject("thumbnailRenderer").getObject("playlistVideoThumbnailRenderer")
                 .getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
 
-        if (url == null || url.isEmpty()) {
+        if (isNullOrEmpty(url)) {
             url = initialData.getObject("microformat").getObject("microformatDataRenderer").getObject("thumbnail")
                     .getArray("thumbnails").getObject(0).getString("url");
 
-            if (url == null || url.isEmpty()) throw new ParsingException("Could not get playlist thumbnail");
+            if (isNullOrEmpty(url)) throw new ParsingException("Could not get playlist thumbnail");
         }
 
         return fixThumbnailUrl(url);
@@ -149,29 +158,60 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
 
     @Nonnull
     @Override
-    public InfoItemsPage getInitialPage() {
-        StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
+    public String getSubChannelName() {
+        return "";
+    }
 
-        JsonArray videos = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
+    @Nonnull
+    @Override
+    public String getSubChannelUrl() {
+        return "";
+    }
+
+    @Nonnull
+    @Override
+    public String getSubChannelAvatarUrl() {
+        return "";
+    }
+
+    @Nonnull
+    @Override
+    public InfoItemsPage getInitialPage() {
+        final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
+
+        final JsonArray contents = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
                 .getArray("tabs").getObject(0).getObject("tabRenderer").getObject("content")
                 .getObject("sectionListRenderer").getArray("contents").getObject(0)
-                .getObject("itemSectionRenderer").getArray("contents").getObject(0)
-                .getObject("playlistVideoListRenderer").getArray("contents");
+                .getObject("itemSectionRenderer").getArray("contents");
+
+        if (contents.getObject(0).has("playlistSegmentRenderer")) {
+            for (final Object segment : contents) {
+                if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("trailer")) {
+                    collectTrailerFrom(collector, ((JsonObject) segment));
+                } else if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("videoList")) {
+                    collectStreamsFrom(collector, ((JsonObject) segment).getObject("playlistSegmentRenderer")
+                            .getObject("videoList").getObject("playlistVideoListRenderer").getArray("contents"));
+                }
+            }
+        } else if (contents.getObject(0).has("playlistVideoListRenderer")) {
+            final JsonArray videos = contents.getObject(0)
+                    .getObject("playlistVideoListRenderer").getArray("contents");
+            collectStreamsFrom(collector, videos);
+        }
 
-        collectStreamsFrom(collector, videos);
         return new InfoItemsPage<>(collector, getNextPageUrl());
     }
 
     @Override
     public InfoItemsPage getPage(final String pageUrl) throws IOException, ExtractionException {
-        if (pageUrl == null || pageUrl.isEmpty()) {
+        if (isNullOrEmpty(pageUrl)) {
             throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
         }
 
-        StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
+        final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
         final JsonArray ajaxJson = getJsonResponse(pageUrl, getExtractorLocalization());
 
-        JsonObject sectionListContinuation = ajaxJson.getObject(1).getObject("response")
+        final JsonObject sectionListContinuation = ajaxJson.getObject(1).getObject("response")
                 .getObject("continuationContents").getObject("playlistVideoListContinuation");
 
         collectStreamsFrom(collector, sectionListContinuation.getArray("contents"));
@@ -179,8 +219,8 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
         return new InfoItemsPage<>(collector, getNextPageUrlFrom(sectionListContinuation.getArray("continuations")));
     }
 
-    private String getNextPageUrlFrom(JsonArray continuations) {
-        if (continuations == null || continuations.isEmpty()) {
+    private String getNextPageUrlFrom(final JsonArray continuations) {
+        if (isNullOrEmpty(continuations)) {
             return "";
         }
 
@@ -191,9 +231,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
                 + "&itct=" + clickTrackingParams;
     }
 
-    private void collectStreamsFrom(StreamInfoItemsCollector collector, JsonArray videos) {
-        collector.reset();
-
+    private void collectStreamsFrom(final StreamInfoItemsCollector collector, final JsonArray videos) {
         final TimeAgoParser timeAgoParser = getTimeAgoParser();
 
         for (Object video : videos) {
@@ -207,4 +245,76 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
             }
         }
     }
+
+    private void collectTrailerFrom(final StreamInfoItemsCollector collector,
+                                    final JsonObject segment) {
+        collector.commit(new StreamInfoItemExtractor() {
+            @Override
+            public String getName() throws ParsingException {
+                return getTextFromObject(segment.getObject("playlistSegmentRenderer")
+                        .getObject("title"));
+            }
+
+            @Override
+            public String getUrl() throws ParsingException {
+                return YoutubeStreamLinkHandlerFactory.getInstance()
+                        .fromId(segment.getObject("playlistSegmentRenderer").getObject("trailer")
+                                .getObject("playlistVideoPlayerRenderer").getString("videoId"))
+                        .getUrl();
+            }
+
+            @Override
+            public String getThumbnailUrl() {
+                final JsonArray thumbnails = initialAjaxJson.getObject(1).getObject("playerResponse")
+                        .getObject("videoDetails").getObject("thumbnail").getArray("thumbnails");
+                // the last thumbnail is the one with the highest resolution
+                final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
+                return fixThumbnailUrl(url);
+            }
+
+            @Override
+            public StreamType getStreamType() {
+                return StreamType.VIDEO_STREAM;
+            }
+
+            @Override
+            public boolean isAd() {
+                return false;
+            }
+
+            @Override
+            public long getDuration() throws ParsingException {
+                return YoutubeParsingHelper.parseDurationString(
+                        getTextFromObject(segment.getObject("playlistSegmentRenderer")
+                                .getObject("segmentAnnotation")).split("•")[0]);
+            }
+
+            @Override
+            public long getViewCount() {
+                return -1;
+            }
+
+            @Override
+            public String getUploaderName() throws ParsingException {
+                return YoutubePlaylistExtractor.this.getUploaderName();
+            }
+
+            @Override
+            public String getUploaderUrl() throws ParsingException {
+                return YoutubePlaylistExtractor.this.getUploaderUrl();
+            }
+
+            @Nullable
+            @Override
+            public String getTextualUploadDate() {
+                return null;
+            }
+
+            @Nullable
+            @Override
+            public DateWrapper getUploadDate() {
+                return null;
+            }
+        });
+    }
 }
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistInfoItemExtractor.java
index 5b4e7afc8..5ce475075 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistInfoItemExtractor.java
@@ -7,8 +7,8 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
 import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubePlaylistLinkHandlerFactory;
 import org.schabi.newpipe.extractor.utils.Utils;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.fixThumbnailUrl;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
 
 public class YoutubePlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
     private JsonObject playlistInfoItem;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeSearchExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeSearchExtractor.java
index 633cfda6f..560943628 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeSearchExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeSearchExtractor.java
@@ -2,7 +2,6 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
 
 import com.grack.nanojson.JsonArray;
 import com.grack.nanojson.JsonObject;
-
 import org.schabi.newpipe.extractor.InfoItem;
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -12,13 +11,14 @@ import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
 import org.schabi.newpipe.extractor.localization.TimeAgoParser;
 import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
 import org.schabi.newpipe.extractor.search.SearchExtractor;
-
-import java.io.IOException;
+import org.schabi.newpipe.extractor.utils.JsonUtils;
 
 import javax.annotation.Nonnull;
+import java.io.IOException;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 /*
  * Created by Christian Schabesberger on 22.07.2018
@@ -62,17 +62,35 @@ public class YoutubeSearchExtractor extends SearchExtractor {
         return super.getUrl() + "&gl=" + getExtractorContentCountry().getCountryCode();
     }
 
+    @Nonnull
     @Override
     public String getSearchSuggestion() throws ParsingException {
+        final JsonObject itemSectionRenderer = initialData.getObject("contents")
+                .getObject("twoColumnSearchResultsRenderer").getObject("primaryContents")
+                .getObject("sectionListRenderer").getArray("contents").getObject(0)
+                .getObject("itemSectionRenderer");
+        final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents").getObject(0)
+                .getObject("didYouMeanRenderer");
+        final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
+                .getObject("showingResultsForRenderer");
+
+        if (!didYouMeanRenderer.isEmpty()) {
+            return JsonUtils.getString(didYouMeanRenderer, "correctedQueryEndpoint.searchEndpoint.query");
+        } else if (showingResultsForRenderer != null) {
+            return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery"));
+        } else {
+            return "";
+        }
+    }
+
+    @Override
+    public boolean isCorrectedSearch() {
         final JsonObject showingResultsForRenderer = initialData.getObject("contents")
                 .getObject("twoColumnSearchResultsRenderer").getObject("primaryContents")
                 .getObject("sectionListRenderer").getArray("contents").getObject(0)
                 .getObject("itemSectionRenderer").getArray("contents").getObject(0)
                 .getObject("showingResultsForRenderer");
-        if (!showingResultsForRenderer.has("correctedQuery")) {
-            return "";
-        }
-        return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery"));
+        return !showingResultsForRenderer.isEmpty();
     }
 
     @Nonnull
@@ -99,7 +117,7 @@ public class YoutubeSearchExtractor extends SearchExtractor {
 
     @Override
     public InfoItemsPage getPage(final String pageUrl) throws IOException, ExtractionException {
-        if (pageUrl == null || pageUrl.isEmpty()) {
+        if (isNullOrEmpty(pageUrl)) {
             throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
         }
 
@@ -133,7 +151,7 @@ public class YoutubeSearchExtractor extends SearchExtractor {
     }
 
     private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException {
-        if (continuations == null || continuations.isEmpty()) {
+        if (isNullOrEmpty(continuations)) {
             return "";
         }
 
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java
index 55268ed73..bdc2a10f3 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
 import com.grack.nanojson.JsonArray;
 import com.grack.nanojson.JsonObject;
 import com.grack.nanojson.JsonParser;
+
 import org.mozilla.javascript.Context;
 import org.mozilla.javascript.Function;
 import org.mozilla.javascript.ScriptableObject;
@@ -21,7 +22,7 @@ import org.schabi.newpipe.extractor.localization.TimeAgoParser;
 import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
 import org.schabi.newpipe.extractor.services.youtube.ItagItem;
 import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.stream.AudioStream;
 import org.schabi.newpipe.extractor.stream.Description;
 import org.schabi.newpipe.extractor.stream.Frameset;
@@ -35,8 +36,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
 import org.schabi.newpipe.extractor.utils.Parser;
 import org.schabi.newpipe.extractor.utils.Utils;
 
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.text.SimpleDateFormat;
@@ -50,11 +49,12 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.fixThumbnailUrl;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
 import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 /*
  * Created by Christian Schabesberger on 06.08.15.
@@ -117,10 +117,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
         assertPageFetched();
         String title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
 
-        if (title.isEmpty()) {
+        if (isNullOrEmpty(title)) {
             title = playerResponse.getObject("videoDetails").getString("title");
 
-            if (title == null || title.isEmpty()) throw new ParsingException("Could not get name");
+            if (isNullOrEmpty(title)) throw new ParsingException("Could not get name");
         }
 
         return title;
@@ -168,7 +168,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
     public DateWrapper getUploadDate() throws ParsingException {
         final String textualUploadDate = getTextualUploadDate();
 
-        if (textualUploadDate == null || textualUploadDate.isEmpty()) {
+        if (isNullOrEmpty(textualUploadDate)) {
             return null;
         }
 
@@ -197,7 +197,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
         assertPageFetched();
         // description with more info on links
         String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true);
-        if (!description.isEmpty()) return new Description(description, Description.HTML);
+        if (description != null && !description.isEmpty()) return new Description(description, Description.HTML);
 
         // raw non-html description
         return new Description(playerResponse.getObject("videoDetails").getString("shortDescription"), Description.PLAIN_TEXT);
@@ -205,7 +205,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
 
     @Override
     public int getAgeLimit() {
-        if (initialData == null || initialData.isEmpty()) throw new IllegalStateException("initialData is not parsed yet");
+        if (isNullOrEmpty(initialData)) throw new IllegalStateException("initialData is not parsed yet");
 
         return ageLimit;
     }
@@ -249,10 +249,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
         String views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
                     .getObject("videoViewCountRenderer").getObject("viewCount"));
 
-        if (views.isEmpty()) {
+        if (isNullOrEmpty(views)) {
             views = playerResponse.getObject("videoDetails").getString("viewCount");
 
-            if (views == null || views.isEmpty()) throw new ParsingException("Could not get view count");
+            if (isNullOrEmpty(views)) throw new ParsingException("Could not get view count");
         }
 
         if (views.toLowerCase().contains("no views")) return 0;
@@ -330,10 +330,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
         String uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
                     .getObject("videoOwnerRenderer").getObject("title"));
 
-        if (uploaderName.isEmpty()) {
+        if (isNullOrEmpty(uploaderName)) {
             uploaderName = playerResponse.getObject("videoDetails").getString("author");
 
-            if (uploaderName == null || uploaderName.isEmpty()) throw new ParsingException("Could not get uploader name");
+            if (isNullOrEmpty(uploaderName)) throw new ParsingException("Could not get uploader name");
         }
 
         return uploaderName;
@@ -353,13 +353,33 @@ public class YoutubeStreamExtractor extends StreamExtractor {
         }
     }
 
+    @Nonnull
+    @Override
+    public String getSubChannelUrl() throws ParsingException {
+        return "";
+    }
+
+    @Nonnull
+    @Override
+    public String getSubChannelName() throws ParsingException {
+        return "";
+    }
+
+    @Nonnull
+    @Override
+    public String getSubChannelAvatarUrl() throws ParsingException {
+        return "";
+    }
+
     @Nonnull
     @Override
     public String getDashMpdUrl() throws ParsingException {
         assertPageFetched();
         try {
             String dashManifestUrl;
-            if (videoInfoPage.containsKey("dashmpd")) {
+            if (playerResponse.getObject("streamingData").isString("dashManifestUrl")) {
+                return playerResponse.getObject("streamingData").getString("dashManifestUrl");
+            } else if (videoInfoPage.containsKey("dashmpd")) {
                 dashManifestUrl = videoInfoPage.get("dashmpd");
             } else if (playerArgs != null && playerArgs.isString("dashmpd")) {
                 dashManifestUrl = playerArgs.getString("dashmpd", EMPTY_STRING);
@@ -910,8 +930,12 @@ public class YoutubeStreamExtractor extends StreamExtractor {
                             streamUrl = formatData.getString("url");
                         } else {
                             // this url has an encrypted signature
-                            Map cipher = Parser.compatParseMap(formatData.getString("cipher"));
-                            streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" + decryptSignature(cipher.get("s"), decryptionCode);
+                            final String cipherString = formatData.has("cipher")
+                                    ? formatData.getString("cipher")
+                                    : formatData.getString("signatureCipher");
+                            final Map cipher = Parser.compatParseMap(cipherString);
+                            streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+                                    + decryptSignature(cipher.get("s"), decryptionCode);
                         }
 
                         urlAndItags.put(streamUrl, itagItem);
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
index 787027692..0757208ee 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
@@ -5,7 +5,7 @@ import com.grack.nanojson.JsonObject;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.localization.DateWrapper;
 import org.schabi.newpipe.extractor.localization.TimeAgoParser;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
 import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
 import org.schabi.newpipe.extractor.stream.StreamType;
@@ -16,8 +16,9 @@ import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.*;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
 import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 /*
  * Copyright (C) Christian Schabesberger 2016 
@@ -93,7 +94,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
     @Override
     public String getName() throws ParsingException {
         String name = getTextFromObject(videoInfo.getObject("title"));
-        if (!name.isEmpty()) return name;
+        if (!isNullOrEmpty(name)) return name;
         throw new ParsingException("Could not get name");
     }
 
@@ -105,7 +106,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
 
         String duration = getTextFromObject(videoInfo.getObject("lengthText"));
 
-        if (duration.isEmpty()) {
+        if (isNullOrEmpty(duration)) {
             for (Object thumbnailOverlay : videoInfo.getArray("thumbnailOverlays")) {
                 if (((JsonObject) thumbnailOverlay).has("thumbnailOverlayTimeStatusRenderer")) {
                     duration = getTextFromObject(((JsonObject) thumbnailOverlay)
@@ -113,7 +114,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
                 }
             }
 
-            if (duration.isEmpty()) throw new ParsingException("Could not get duration");
+            if (isNullOrEmpty(duration)) throw new ParsingException("Could not get duration");
         }
 
         return YoutubeParsingHelper.parseDurationString(duration);
@@ -123,13 +124,13 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
     public String getUploaderName() throws ParsingException {
         String name = getTextFromObject(videoInfo.getObject("longBylineText"));
 
-        if (name.isEmpty()) {
+        if (isNullOrEmpty(name)) {
             name = getTextFromObject(videoInfo.getObject("ownerText"));
 
-            if (name.isEmpty()) {
+            if (isNullOrEmpty(name)) {
                 name = getTextFromObject(videoInfo.getObject("shortBylineText"));
 
-                if (name.isEmpty()) throw new ParsingException("Could not get uploader name");
+                if (isNullOrEmpty(name)) throw new ParsingException("Could not get uploader name");
             }
         }
 
@@ -141,15 +142,15 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
         String url = getUrlFromNavigationEndpoint(videoInfo.getObject("longBylineText")
                 .getArray("runs").getObject(0).getObject("navigationEndpoint"));
 
-        if (url == null || url.isEmpty()) {
+        if (isNullOrEmpty(url)) {
             url = getUrlFromNavigationEndpoint(videoInfo.getObject("ownerText")
                     .getArray("runs").getObject(0).getObject("navigationEndpoint"));
 
-            if (url == null || url.isEmpty()) {
+            if (isNullOrEmpty(url)) {
                 url = getUrlFromNavigationEndpoint(videoInfo.getObject("shortBylineText")
                         .getArray("runs").getObject(0).getObject("navigationEndpoint"));
 
-                if (url == null || url.isEmpty()) throw new ParsingException("Could not get uploader url");
+                if (isNullOrEmpty(url)) throw new ParsingException("Could not get uploader url");
             }
         }
 
@@ -169,7 +170,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
         }
 
         final String publishedTimeText = getTextFromObject(videoInfo.getObject("publishedTimeText"));
-        if (!publishedTimeText.isEmpty()) return publishedTimeText;
+        if (publishedTimeText != null && !publishedTimeText.isEmpty()) return publishedTimeText;
 
         return null;
     }
@@ -186,7 +187,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
         }
 
         final String textualUploadDate = getTextualUploadDate();
-        if (timeAgoParser != null && textualUploadDate != null && !textualUploadDate.isEmpty()) {
+        if (timeAgoParser != null && !isNullOrEmpty(textualUploadDate)) {
             try {
                 return timeAgoParser.parse(textualUploadDate);
             } catch (ParsingException e) {
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeTrendingExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeTrendingExtractor.java
index 3a015ba9c..ddd108e87 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeTrendingExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeTrendingExtractor.java
@@ -22,6 +22,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
 
 import com.grack.nanojson.JsonArray;
 import com.grack.nanojson.JsonObject;
+
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.downloader.Downloader;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -32,11 +33,13 @@ import org.schabi.newpipe.extractor.localization.TimeAgoParser;
 import org.schabi.newpipe.extractor.stream.StreamInfoItem;
 import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
 
-import javax.annotation.Nonnull;
 import java.io.IOException;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse;
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
+import javax.annotation.Nonnull;
+
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 public class YoutubeTrendingExtractor extends KioskExtractor {
     private JsonObject initialData;
@@ -71,7 +74,7 @@ public class YoutubeTrendingExtractor extends KioskExtractor {
     @Override
     public String getName() throws ParsingException {
         String name = getTextFromObject(initialData.getObject("header").getObject("feedTabbedHeaderRenderer").getObject("title"));
-        if (!name.isEmpty()) {
+        if (!isNullOrEmpty(name)) {
             return name;
         }
         throw new ParsingException("Could not get Trending name");
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeChannelLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeChannelLinkHandlerFactory.java
index 77eaf0694..0eb030852 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeChannelLinkHandlerFactory.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeChannelLinkHandlerFactory.java
@@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor.services.youtube.linkHandler;
 
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.net.URL;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeCommentsLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeCommentsLinkHandlerFactory.java
index 43baf6a96..15bc31b66 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeCommentsLinkHandlerFactory.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeCommentsLinkHandlerFactory.java
@@ -1,6 +1,6 @@
 package org.schabi.newpipe.extractor.services.youtube.linkHandler;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.BASE_YOUTUBE_INTENT_URL;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.BASE_YOUTUBE_INTENT_URL;
 
 import org.schabi.newpipe.extractor.exceptions.FoundAdException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java
index 1950f697b..56abc194b 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.youtube.linkHandler;
 import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.net.URL;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeStreamLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeStreamLinkHandlerFactory.java
index fd5b5b46d..7f3e6824e 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeStreamLinkHandlerFactory.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeStreamLinkHandlerFactory.java
@@ -1,11 +1,12 @@
 package org.schabi.newpipe.extractor.services.youtube.linkHandler;
 
-import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.BASE_YOUTUBE_INTENT_URL;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.BASE_YOUTUBE_INTENT_URL;
 
 import org.schabi.newpipe.extractor.exceptions.FoundAdException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
 import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.net.MalformedURLException;
@@ -13,6 +14,8 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 
+import javax.annotation.Nullable;
+
 /*
  * Created by Christian Schabesberger on 02.02.16.
  *
@@ -44,12 +47,16 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
         return instance;
     }
 
-    private static String assertIsID(String id) throws ParsingException {
-        if (id == null || !id.matches("[a-zA-Z0-9_-]{11}")) {
+    private static boolean isId(@Nullable String id) {
+        return id != null && id.matches("[a-zA-Z0-9_-]{11}");
+    }
+
+    private static String assertIsId(@Nullable String id) throws ParsingException {
+        if (isId(id)) {
+            return id;
+        } else {
             throw new ParsingException("The given string is not a Youtube-Video-ID");
         }
-
-        return id;
     }
 
     @Override
@@ -75,9 +82,14 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
             if (scheme != null && (scheme.equals("vnd.youtube") || scheme.equals("vnd.youtube.launch"))) {
                 String schemeSpecificPart = uri.getSchemeSpecificPart();
                 if (schemeSpecificPart.startsWith("//")) {
+                    final String possiblyId = schemeSpecificPart.substring(2);
+                    if (isId(possiblyId)) {
+                        return possiblyId;
+                    }
+
                     urlString = "https:" + schemeSpecificPart;
                 } else {
-                    return assertIsID(schemeSpecificPart);
+                    return assertIsId(schemeSpecificPart);
                 }
             }
         } catch (URISyntaxException ignored) {
@@ -118,7 +130,7 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
                 if (path.startsWith("embed/")) {
                     String id = path.split("/")[1];
 
-                    return assertIsID(id);
+                    return assertIsId(id);
                 }
 
                 break;
@@ -139,38 +151,38 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
                     }
 
                     String viewQueryValue = Utils.getQueryValue(decodedURL, "v");
-                    return assertIsID(viewQueryValue);
+                    return assertIsId(viewQueryValue);
                 }
 
                 if (path.startsWith("embed/")) {
                     String id = path.split("/")[1];
 
-                    return assertIsID(id);
+                    return assertIsId(id);
                 }
 
                 String viewQueryValue = Utils.getQueryValue(url, "v");
-                return assertIsID(viewQueryValue);
+                return assertIsId(viewQueryValue);
             }
 
             case "YOUTU.BE": {
                 String viewQueryValue = Utils.getQueryValue(url, "v");
                 if (viewQueryValue != null) {
-                    return assertIsID(viewQueryValue);
+                    return assertIsId(viewQueryValue);
                 }
 
-                return assertIsID(path);
+                return assertIsId(path);
             }
 
             case "HOOKTUBE.COM": {
                 if (path.startsWith("v/")) {
                     String id = path.substring("v/".length());
 
-                    return assertIsID(id);
+                    return assertIsId(id);
                 }
                 if (path.startsWith("watch/")) {
                     String id = path.substring("watch/".length());
 
-                    return assertIsID(id);
+                    return assertIsId(id);
                 }
                 // there is no break-statement here on purpose so the next code-block gets also run for hooktube
             }
@@ -193,21 +205,21 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
                 if (path.equals("watch")) {
                     String viewQueryValue = Utils.getQueryValue(url, "v");
                     if (viewQueryValue != null) {
-                        return assertIsID(viewQueryValue);
+                        return assertIsId(viewQueryValue);
                     }
                 }
                 if (path.startsWith("embed/")) {
                     String id = path.substring("embed/".length());
 
-                    return assertIsID(id);
+                    return assertIsId(id);
                 }
 
                 String viewQueryValue = Utils.getQueryValue(url, "v");
                 if (viewQueryValue != null) {
-                    return assertIsID(viewQueryValue);
+                    return assertIsId(viewQueryValue);
                 }
 
-                return assertIsID(path);
+                return assertIsId(path);
             }
         }
 
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeTrendingLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeTrendingLinkHandlerFactory.java
index 799681128..6d6db7424 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeTrendingLinkHandlerFactory.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubeTrendingLinkHandlerFactory.java
@@ -21,6 +21,7 @@ package org.schabi.newpipe.extractor.services.youtube.linkHandler;
  */
 
 import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
 import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.net.MalformedURLException;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java
index b48eeab29..7e9908167 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java
@@ -5,6 +5,8 @@ import org.schabi.newpipe.extractor.MediaFormat;
 import java.io.Serializable;
 import java.util.List;
 
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
 /**
  * Creates a stream object from url, format and optional torrent url
  */
@@ -61,7 +63,7 @@ public abstract class Stream implements Serializable {
      * Check if the list already contains one stream with equals stats
      */
     public static boolean containSimilarStream(Stream stream, List streamList) {
-        if (stream == null || streamList == null) return false;
+        if (isNullOrEmpty(streamList)) return false;
         for (Stream cmpStream : streamList) {
             if (stream.equalStats(cmpStream)) return true;
         }
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
index 78a5fbf3d..2cc64ab70 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
@@ -23,6 +23,7 @@ package org.schabi.newpipe.extractor.stream;
 import org.schabi.newpipe.extractor.Extractor;
 import org.schabi.newpipe.extractor.MediaFormat;
 import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.extractor.channel.ChannelExtractor;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
@@ -148,7 +149,7 @@ public abstract class StreamExtractor extends Extractor {
     /**
      * The Url to the page of the creator/uploader of the stream. This must not be a homepage,
      * but the page offered by the service the extractor handles. This url will be handled by the
-     * ChannelExtractor,
+     * {@link ChannelExtractor},
      * so be sure to implement that one before you return a value here, otherwise NewPipe will crash if one selects
      * this url.
      *
@@ -178,6 +179,39 @@ public abstract class StreamExtractor extends Extractor {
     @Nonnull
     public abstract String getUploaderAvatarUrl() throws ParsingException;
 
+    /**
+     * The Url to the page of the sub-channel of the stream. This must not be a homepage,
+     * but the page offered by the service the extractor handles. This url will be handled by the
+     * {@link ChannelExtractor},
+     * so be sure to implement that one before you return a value here, otherwise NewPipe will crash if one selects
+     * this url.
+     *
+     * @return the url to the page of the sub-channel of the stream or an empty String
+     * @throws ParsingException
+     */
+    @Nonnull
+    public abstract String getSubChannelUrl() throws ParsingException;
+
+    /**
+     * The name of the sub-channel of the stream.
+     * If the name is not available you can simply return an empty string.
+     *
+     * @return the name of the sub-channel of the stream or an empty String
+     * @throws ParsingException
+     */
+    @Nonnull
+    public abstract String getSubChannelName() throws ParsingException;
+
+    /**
+     * The url to the image file/profile picture/avatar of the sub-channel of the stream.
+     * If the url is not available you can return an empty String.
+     *
+     * @return The url of the image file of the sub-channel or an empty String
+     * @throws ParsingException
+     */
+    @Nonnull
+    public abstract String getSubChannelAvatarUrl() throws ParsingException;
+
     /**
      * Get the dash mpd url. If you don't know what a dash MPD is you can read about it
      * here.
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java
index 0e5ff0808..3878e593a 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java
@@ -16,6 +16,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
 /*
  * Created by Christian Schabesberger on 26.08.15.
  *
@@ -103,7 +105,7 @@ public class StreamInfo extends Info {
         String name = extractor.getName();
         int ageLimit = extractor.getAgeLimit();
 
-        if ((streamType == StreamType.NONE) || (url == null || url.isEmpty()) || (id == null || id.isEmpty())
+        if ((streamType == StreamType.NONE) || isNullOrEmpty(url) || (isNullOrEmpty(id))
                 || (name == null /* streamInfo.title can be empty of course */) || (ageLimit == -1)) {
             throw new ExtractionException("Some important stream information was not given.");
         }
@@ -159,7 +161,7 @@ public class StreamInfo extends Info {
             streamInfo.setAudioStreams(new ArrayList());
 
         Exception dashMpdError = null;
-        if (streamInfo.getDashMpdUrl() != null && !streamInfo.getDashMpdUrl().isEmpty()) {
+        if (!isNullOrEmpty(streamInfo.getDashMpdUrl())) {
             try {
                 DashMpdParser.ParserResult result = DashMpdParser.getStreams(streamInfo);
                 streamInfo.getVideoOnlyStreams().addAll(result.getVideoOnlyStreams());
@@ -223,6 +225,28 @@ public class StreamInfo extends Info {
         } catch (Exception e) {
             streamInfo.addError(e);
         }
+        try {
+            streamInfo.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
+        } catch (Exception e) {
+            streamInfo.addError(e);
+        }
+
+        try {
+            streamInfo.setSubChannelName(extractor.getSubChannelName());
+        } catch (Exception e) {
+            streamInfo.addError(e);
+        }
+        try {
+            streamInfo.setSubChannelUrl(extractor.getSubChannelUrl());
+        } catch (Exception e) {
+            streamInfo.addError(e);
+        }
+        try {
+            streamInfo.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl());
+        } catch (Exception e) {
+            streamInfo.addError(e);
+        }
+
         try {
             streamInfo.setDescription(extractor.getDescription());
         } catch (Exception e) {
@@ -243,11 +267,6 @@ public class StreamInfo extends Info {
         } catch (Exception e) {
             streamInfo.addError(e);
         }
-        try {
-            streamInfo.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
-        } catch (Exception e) {
-            streamInfo.addError(e);
-        }
         try {
             streamInfo.setStartPosition(extractor.getTimeStamp());
         } catch (Exception e) {
@@ -332,6 +351,10 @@ public class StreamInfo extends Info {
     private String uploaderUrl = "";
     private String uploaderAvatarUrl = "";
 
+    private String subChannelName = "";
+    private String subChannelUrl = "";
+    private String subChannelAvatarUrl = "";
+
     private List videoStreams = new ArrayList<>();
     private List audioStreams = new ArrayList<>();
     private List videoOnlyStreams = new ArrayList<>();
@@ -486,6 +509,30 @@ public class StreamInfo extends Info {
         this.uploaderAvatarUrl = uploaderAvatarUrl;
     }
 
+    public String getSubChannelName() {
+        return subChannelName;
+    }
+
+    public void setSubChannelName(String subChannelName) {
+        this.subChannelName = subChannelName;
+    }
+
+    public String getSubChannelUrl() {
+        return subChannelUrl;
+    }
+
+    public void setSubChannelUrl(String subChannelUrl) {
+        this.subChannelUrl = subChannelUrl;
+    }
+
+    public String getSubChannelAvatarUrl() {
+        return subChannelAvatarUrl;
+    }
+
+    public void setSubChannelAvatarUrl(String subChannelAvatarUrl) {
+        this.subChannelAvatarUrl = subChannelAvatarUrl;
+    }
+
     public List getVideoStreams() {
         return videoStreams;
     }
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java
index 76aa2944f..5b70ce59c 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java
@@ -6,7 +6,9 @@ import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLDecoder;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 public class Utils {
 
@@ -48,7 +50,8 @@ public class Utils {
         String multiplier = "";
         try {
             multiplier = Parser.matchGroup("[\\d]+([\\.,][\\d]+)?([KMBkmb])+", numberWord, 2);
-        } catch(ParsingException ignored) {}
+        } catch (ParsingException ignored) {
+        }
         double count = Double.parseDouble(Parser.matchGroup1("([\\d]+([\\.,][\\d]+)?)", numberWord)
                 .replace(",", "."));
         switch (multiplier.toUpperCase()) {
@@ -70,7 +73,7 @@ public class Utils {
      * @param url     the url to be tested
      */
     public static void checkUrl(String pattern, String url) throws ParsingException {
-        if (url == null || url.isEmpty()) {
+        if (isNullOrEmpty(url)) {
             throw new IllegalArgumentException("Url can't be null or empty");
         }
 
@@ -186,4 +189,37 @@ public class Utils {
         }
         return uri.getProtocol() + "://" + uri.getAuthority();
     }
-}
\ No newline at end of file
+
+    public static boolean isNullOrEmpty(final String str) {
+        return str == null || str.isEmpty();
+    }
+
+    // can be used for JsonArrays
+    public static boolean isNullOrEmpty(final Collection collection) {
+        return collection == null || collection.isEmpty();
+    }
+
+    // can be used for JsonObjects
+    public static boolean isNullOrEmpty(final Map map) {
+        return map == null || map.isEmpty();
+    }
+
+    public static boolean isWhitespace(final int c){
+        return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
+    }
+
+    public static boolean isBlank(final String string) {
+        if (string == null || string.isEmpty()) {
+            return true;
+        }
+
+        final int length = string.length();
+        for (int i = 0; i < length; i++) {
+            if (!isWhitespace(string.codePointAt(i))) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseSearchExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseSearchExtractorTest.java
index e82ad7d0a..ddfa27fb0 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseSearchExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseSearchExtractorTest.java
@@ -4,4 +4,5 @@ package org.schabi.newpipe.extractor.services;
 public interface BaseSearchExtractorTest extends BaseListExtractorTest {
     void testSearchString() throws Exception;
     void testSearchSuggestion() throws Exception;
+    void testSearchCorrected() throws Exception;
 }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultSearchExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultSearchExtractorTest.java
index 93c4eac70..8a8a4e53d 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultSearchExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultSearchExtractorTest.java
@@ -8,6 +8,7 @@ import javax.annotation.Nullable;
 
 import static org.junit.Assert.assertEquals;
 import static org.schabi.newpipe.extractor.ExtractorAsserts.assertEmpty;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 public abstract class DefaultSearchExtractorTest extends DefaultListExtractorTest
         implements BaseSearchExtractorTest {
@@ -15,6 +16,10 @@ public abstract class DefaultSearchExtractorTest extends DefaultListExtractorTes
     public abstract String expectedSearchString();
     @Nullable public abstract String expectedSearchSuggestion();
 
+    public boolean isCorrectedSearch() {
+        return false;
+    }
+
     @Test
     @Override
     public void testSearchString() throws Exception {
@@ -25,10 +30,15 @@ public abstract class DefaultSearchExtractorTest extends DefaultListExtractorTes
     @Override
     public void testSearchSuggestion() throws Exception {
         final String expectedSearchSuggestion = expectedSearchSuggestion();
-        if (expectedSearchSuggestion == null || expectedSearchSuggestion.isEmpty()) {
+        if (isNullOrEmpty(expectedSearchSuggestion)) {
             assertEmpty("Suggestion was expected to be empty", extractor().getSearchSuggestion());
         } else {
             assertEquals(expectedSearchSuggestion, extractor().getSearchSuggestion());
         }
     }
+
+    @Test
+    public void testSearchCorrected() throws Exception {
+        assertEquals(isCorrectedSearch(), extractor().isCorrectedSearch());
+    }
 }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultTests.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultTests.java
index a52698415..82f75298a 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultTests.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultTests.java
@@ -18,6 +18,7 @@ import static junit.framework.TestCase.assertFalse;
 import static org.junit.Assert.*;
 import static org.schabi.newpipe.extractor.ExtractorAsserts.*;
 import static org.schabi.newpipe.extractor.StreamingService.LinkType;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 public final class DefaultTests {
     public static void defaultTestListOfItems(StreamingService expectedService, List itemsList, List errors) throws ParsingException {
@@ -27,8 +28,10 @@ public final class DefaultTests {
 
         for (InfoItem item : itemsList) {
             assertIsSecureUrl(item.getUrl());
-            if (item.getThumbnailUrl() != null && !item.getThumbnailUrl().isEmpty()) {
-                assertIsSecureUrl(item.getThumbnailUrl());
+
+            final String thumbnailUrl = item.getThumbnailUrl();
+            if (!isNullOrEmpty(thumbnailUrl)) {
+                assertIsSecureUrl(thumbnailUrl);
             }
             assertNotNull("InfoItem type not set: " + item, item.getInfoType());
             assertEquals("Unexpected item service id", expectedService.getServiceId(), item.getServiceId());
@@ -39,15 +42,15 @@ public final class DefaultTests {
                 assertNotEmpty("Uploader name not set: " + item, streamInfoItem.getUploaderName());
 
 //                assertNotEmpty("Uploader url not set: " + item, streamInfoItem.getUploaderUrl());
-                if (streamInfoItem.getUploaderUrl() != null && !streamInfoItem.getUploaderUrl().isEmpty()) {
-                    assertIsSecureUrl(streamInfoItem.getUploaderUrl());
-                    assertExpectedLinkType(expectedService, streamInfoItem.getUploaderUrl(), LinkType.CHANNEL);
+                final String uploaderUrl = streamInfoItem.getUploaderUrl();
+                if (!isNullOrEmpty(uploaderUrl)) {
+                    assertIsSecureUrl(uploaderUrl);
+                    assertExpectedLinkType(expectedService, uploaderUrl, LinkType.CHANNEL);
                 }
 
                 assertExpectedLinkType(expectedService, streamInfoItem.getUrl(), LinkType.STREAM);
 
-                final String textualUploadDate = streamInfoItem.getTextualUploadDate();
-                if (textualUploadDate != null && !textualUploadDate.isEmpty()) {
+                if (!isNullOrEmpty(streamInfoItem.getTextualUploadDate())) {
                     final DateWrapper uploadDate = streamInfoItem.getUploadDate();
                     assertNotNull("No parsed upload date", uploadDate);
                     assertTrue("Upload date not in the past", uploadDate.date().before(Calendar.getInstance()));
@@ -83,7 +86,7 @@ public final class DefaultTests {
     public static  void assertNoMoreItems(ListExtractor extractor) throws Exception {
         assertFalse("More items available when it shouldn't", extractor.hasNextPage());
         final String nextPageUrl = extractor.getNextPageUrl();
-        assertTrue("Next page is not empty or null", nextPageUrl == null || nextPageUrl.isEmpty());
+        assertTrue("Next page is not empty or null", isNullOrEmpty(nextPageUrl));
     }
 
     public static void assertNoDuplicatedItems(StreamingService expectedService,
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java
index f376b9f4d..95c882290 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java
@@ -76,7 +76,7 @@ public class MediaCCCStreamExtractorTest {
         @Test
         public void testUploaderUrl() throws Exception {
             assertIsSecureUrl(extractor.getUploaderUrl());
-            assertEquals("https://api.media.ccc.de/public/conferences/gpn18", extractor.getUploaderUrl());
+            assertEquals("https://media.ccc.de/public/conferences/gpn18", extractor.getUploaderUrl());
         }
 
         @Test
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeChannelExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeChannelExtractorTest.java
index 9dc4b013e..010dbd7b4 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeChannelExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeChannelExtractorTest.java
@@ -84,6 +84,21 @@ public class PeertubeChannelExtractorTest {
             assertNotNull(extractor.getDescription());
         }
 
+        @Test
+        public void testParentChannelName() throws ParsingException {
+            assertEquals("libux", extractor.getParentChannelName());
+        }
+
+        @Test
+        public void testParentChannelUrl() throws ParsingException {
+            assertEquals("https://peertube.mastodon.host/accounts/libux", extractor.getParentChannelUrl());
+        }
+
+        @Test
+        public void testParentChannelAvatarUrl() throws ParsingException {
+            assertIsSecureUrl(extractor.getParentChannelAvatarUrl());
+        }
+
         @Test
         public void testAvatarUrl() throws ParsingException {
             assertIsSecureUrl(extractor.getAvatarUrl());
@@ -181,6 +196,21 @@ public class PeertubeChannelExtractorTest {
             assertNotNull(extractor.getDescription());
         }
 
+        @Test
+        public void testParentChannelName() throws ParsingException {
+            assertEquals("booteille", extractor.getParentChannelName());
+        }
+
+        @Test
+        public void testParentChannelUrl() throws ParsingException {
+            assertEquals("https://peertube.mastodon.host/accounts/booteille", extractor.getParentChannelUrl());
+        }
+
+        @Test
+        public void testParentChannelAvatarUrl() throws ParsingException {
+            assertIsSecureUrl(extractor.getParentChannelAvatarUrl());
+        }
+
         @Test
         public void testAvatarUrl() throws ParsingException {
             assertIsSecureUrl(extractor.getAvatarUrl());
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeCommentsExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeCommentsExtractorTest.java
index 1241b0a86..7e4923995 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeCommentsExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeCommentsExtractorTest.java
@@ -1,6 +1,5 @@
 package org.schabi.newpipe.extractor.services.peertube;
 
-import org.jsoup.helper.StringUtil;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
@@ -10,12 +9,12 @@ import org.schabi.newpipe.extractor.comments.CommentsInfo;
 import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeCommentsExtractor;
+import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.io.IOException;
 import java.util.List;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 import static org.schabi.newpipe.extractor.ServiceList.PeerTube;
 
 public class PeertubeCommentsExtractorTest {
@@ -47,11 +46,11 @@ public class PeertubeCommentsExtractorTest {
     public void testGetCommentsFromCommentsInfo() throws IOException, ExtractionException {
         boolean result = false;
         CommentsInfo commentsInfo = CommentsInfo.getInfo("https://framatube.org/videos/watch/a8ea95b8-0396-49a6-8f30-e25e25fb2828");
-        assertTrue("Comments".equals(commentsInfo.getName()));
+        assertEquals("Comments", commentsInfo.getName());
         result = findInComments(commentsInfo.getRelatedItems(), "Loved it!!!");
 
         String nextPage = commentsInfo.getNextPageUrl();
-        while (!StringUtil.isBlank(nextPage) && !result) {
+        while (!Utils.isBlank(nextPage) && !result) {
             InfoItemsPage moreItems = CommentsInfo.getMoreItems(PeerTube, commentsInfo, nextPage);
             result = findInComments(moreItems.getItems(), "Loved it!!!");
             nextPage = moreItems.getNextPageUrl();
@@ -64,15 +63,15 @@ public class PeertubeCommentsExtractorTest {
     public void testGetCommentsAllData() throws IOException, ExtractionException {
         InfoItemsPage comments = extractor.getInitialPage();
         for (CommentsInfoItem c : comments.getItems()) {
-            assertFalse(StringUtil.isBlank(c.getAuthorEndpoint()));
-            assertFalse(StringUtil.isBlank(c.getAuthorName()));
-            assertFalse(StringUtil.isBlank(c.getAuthorThumbnail()));
-            assertFalse(StringUtil.isBlank(c.getCommentId()));
-            assertFalse(StringUtil.isBlank(c.getCommentText()));
-            assertFalse(StringUtil.isBlank(c.getName()));
-            assertFalse(StringUtil.isBlank(c.getTextualPublishedTime()));
-            assertFalse(StringUtil.isBlank(c.getThumbnailUrl()));
-            assertFalse(StringUtil.isBlank(c.getUrl()));
+            assertFalse(Utils.isBlank(c.getUploaderUrl()));
+            assertFalse(Utils.isBlank(c.getUploaderName()));
+            assertFalse(Utils.isBlank(c.getUploaderAvatarUrl()));
+            assertFalse(Utils.isBlank(c.getCommentId()));
+            assertFalse(Utils.isBlank(c.getCommentText()));
+            assertFalse(Utils.isBlank(c.getName()));
+            assertFalse(Utils.isBlank(c.getTextualUploadDate()));
+            assertFalse(Utils.isBlank(c.getThumbnailUrl()));
+            assertFalse(Utils.isBlank(c.getUrl()));
             assertFalse(c.getLikeCount() != -1);
         }
     }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistExtractorTest.java
new file mode 100644
index 000000000..47e2960fc
--- /dev/null
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistExtractorTest.java
@@ -0,0 +1,72 @@
+package org.schabi.newpipe.extractor.services.peertube;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.schabi.newpipe.DownloaderTestImpl;
+import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubePlaylistExtractor;
+
+import static org.junit.Assert.assertEquals;
+import static org.schabi.newpipe.extractor.ServiceList.PeerTube;
+
+public class PeertubePlaylistExtractorTest {
+
+    public static class Shocking {
+        private static PeertubePlaylistExtractor extractor;
+
+        @BeforeClass
+        public static void setUp() throws Exception {
+            NewPipe.init(DownloaderTestImpl.getInstance());
+            extractor = (PeertubePlaylistExtractor) PeerTube
+                    .getPlaylistExtractor("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7");
+            extractor.fetchPage();
+        }
+
+        @Test
+        public void testGetName() throws ParsingException {
+            assertEquals("Shocking !", extractor.getName());
+        }
+
+        @Test
+        public void testGetThumbnailUrl() throws ParsingException {
+            assertEquals("https://framatube.org/static/thumbnails/playlist-96b0ee2b-a5a7-4794-8769-58d8ccb79ab7.jpg", extractor.getThumbnailUrl());
+        }
+
+        @Test
+        public void testGetUploaderUrl() throws ParsingException {
+            assertEquals("https://skeptikon.fr/accounts/metadechoc", extractor.getUploaderUrl());
+        }
+
+        @Test
+        public void testGetUploaderAvatarUrl() throws ParsingException {
+            assertEquals("https://framatube.org/lazy-static/avatars/cd0f781d-0287-4be2-94f1-24cd732337b2.jpg", extractor.getUploaderAvatarUrl());
+        }
+
+        @Test
+        public void testGetUploaderName() throws ParsingException {
+            assertEquals("Méta de Choc", extractor.getUploaderName());
+        }
+
+        @Test
+        public void testGetStreamCount() throws ParsingException {
+            assertEquals(35, extractor.getStreamCount());
+        }
+
+        @Test
+        public void testGetSubChannelUrl() throws ParsingException {
+            assertEquals("https://skeptikon.fr/video-channels/metadechoc_channel", extractor.getSubChannelUrl());
+        }
+
+        @Test
+        public void testGetSubChannelName() throws ParsingException {
+            assertEquals("SHOCKING !", extractor.getSubChannelName());
+        }
+
+        @Test
+        public void testGetSubChannelAvatarUrl() throws ParsingException {
+            assertEquals("https://framatube.org/lazy-static/avatars/f1dcd0e8-e651-42ed-ae81-bb3bd4aff2bc.png",
+                    extractor.getSubChannelAvatarUrl());
+        }
+    }
+}
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistLinkHandlerFactoryTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistLinkHandlerFactoryTest.java
index ac04a01ee..4b1cc38a4 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistLinkHandlerFactoryTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubePlaylistLinkHandlerFactoryTest.java
@@ -25,12 +25,17 @@ public class PeertubePlaylistLinkHandlerFactoryTest {
 
     @Test
     public void acceptUrlTest() throws ParsingException {
-        assertTrue(linkHandler.acceptUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33/videos"));
+        assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909"));
+        assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909/videos"));
+        assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/dacdc4ef-5160-4846-9b70-a655880da667"));
+        assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7"));
     }
 
     @Test
     public void getIdFromUrl() throws ParsingException {
-        assertEquals("b45e84fb-c47f-475b-94f2-718126154d33", linkHandler.fromUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33").getId());
-        assertEquals("b45e84fb-c47f-475b-94f2-718126154d33", linkHandler.fromUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33/videos").getId());
+        assertEquals("d8ca79f9-e4c7-4269-8183-d78ed269c909", linkHandler.getId("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909"));
+        assertEquals("dacdc4ef-5160-4846-9b70-a655880da667", linkHandler.getId("https://framatube.org/videos/watch/playlist/dacdc4ef-5160-4846-9b70-a655880da667"));
+        assertEquals("bfc145f5-1be7-48a6-9b9e-4f1967199dad", linkHandler.getId("https://framatube.org/videos/watch/playlist/bfc145f5-1be7-48a6-9b9e-4f1967199dad"));
+        assertEquals("96b0ee2b-a5a7-4794-8769-58d8ccb79ab7", linkHandler.getId("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7"));
     }
 }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorDefaultTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorDefaultTest.java
index f5202f39d..dd4c9c449 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorDefaultTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorDefaultTest.java
@@ -87,6 +87,33 @@ public class PeertubeStreamExtractorDefaultTest {
         assertEquals("Framasoft", extractor.getUploaderName());
     }
 
+    @Test
+    public void testGetUploaderUrl() throws ParsingException {
+        assertIsSecureUrl(extractor.getUploaderUrl());
+        assertEquals("https://framatube.org/api/v1/accounts/framasoft@framatube.org", extractor.getUploaderUrl());
+    }
+
+    @Test
+    public void testGetUploaderAvatarUrl() throws ParsingException {
+        assertIsSecureUrl(extractor.getUploaderAvatarUrl());
+    }
+
+    @Test
+    public void testGetSubChannelName() throws ParsingException {
+        assertEquals("Les vidéos de Framasoft", extractor.getSubChannelName());
+    }
+
+    @Test
+    public void testGetSubChannelUrl() throws ParsingException {
+        assertIsSecureUrl(extractor.getSubChannelUrl());
+        assertEquals("https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8", extractor.getSubChannelUrl());
+    }
+
+    @Test
+    public void testGetSubChannelAvatarUrl() throws ParsingException {
+        assertIsSecureUrl(extractor.getSubChannelAvatarUrl());
+    }
+
     @Test
     public void testGetLength() throws ParsingException {
         assertEquals(113, extractor.getLength());
@@ -98,22 +125,11 @@ public class PeertubeStreamExtractorDefaultTest {
                 extractor.getViewCount() > 10);
     }
 
-    @Test
-    public void testGetUploaderUrl() throws ParsingException {
-        assertIsSecureUrl(extractor.getUploaderUrl());
-        assertEquals("https://framatube.org/api/v1/accounts/framasoft@framatube.org", extractor.getUploaderUrl());
-    }
-
     @Test
     public void testGetThumbnailUrl() throws ParsingException {
         assertIsSecureUrl(extractor.getThumbnailUrl());
     }
 
-    @Test
-    public void testGetUploaderAvatarUrl() throws ParsingException {
-        assertIsSecureUrl(extractor.getUploaderAvatarUrl());
-    }
-
     @Test
     public void testGetVideoStreams() throws IOException, ExtractionException {
         assertFalse(extractor.getVideoStreams().isEmpty());
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeTrendingExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeTrendingExtractorTest.java
index e7334fca4..60f72f9d1 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeTrendingExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeTrendingExtractorTest.java
@@ -3,17 +3,10 @@ package org.schabi.newpipe.extractor.services.peertube;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
-import org.schabi.newpipe.extractor.ListExtractor;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
-import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
 import org.schabi.newpipe.extractor.services.BaseListExtractorTest;
 import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeTrendingExtractor;
-import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudChartsExtractor;
-import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeTrendingExtractor;
-import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-
-import java.util.List;
 
 import static org.junit.Assert.*;
 import static org.schabi.newpipe.extractor.ServiceList.*;
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractorTest.java
index 485462fbd..1877e11ff 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChannelExtractorTest.java
@@ -7,6 +7,7 @@ import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.channel.ChannelExtractor;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.services.BaseChannelExtractorTest;
+import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelExtractor;
 
 import static org.junit.Assert.*;
 import static org.schabi.newpipe.extractor.ExtractorAsserts.assertEmpty;
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractorTest.java
index 189be4172..8abfb4d3f 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsExtractorTest.java
@@ -3,19 +3,13 @@ package org.schabi.newpipe.extractor.services.soundcloud;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
-import org.schabi.newpipe.extractor.ListExtractor;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
-import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
 import org.schabi.newpipe.extractor.services.BaseListExtractorTest;
-import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeTrendingExtractor;
-import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-
-import java.util.List;
+import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChartsExtractor;
 
 import static org.junit.Assert.*;
 import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
-import static org.schabi.newpipe.extractor.ServiceList.YouTube;
 import static org.schabi.newpipe.extractor.services.DefaultTests.*;
 
 public class SoundcloudChartsExtractorTest {
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactoryTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactoryTest.java
index 6bcc2e73f..97cc2a80b 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactoryTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudChartsLinkHandlerFactoryTest.java
@@ -5,6 +5,7 @@ import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChartsLinkHandlerFactory;
 
 import static junit.framework.TestCase.assertFalse;
 import static org.junit.Assert.assertEquals;
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractorTest.java
index 59ecff492..6a366fd30 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudPlaylistExtractorTest.java
@@ -7,6 +7,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
 import org.schabi.newpipe.extractor.services.BasePlaylistExtractorTest;
+import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistExtractor;
 import org.schabi.newpipe.extractor.stream.StreamInfoItem;
 
 import static org.hamcrest.CoreMatchers.*;
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorDefaultTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorDefaultTest.java
index 75178c4c9..300d37572 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorDefaultTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorDefaultTest.java
@@ -8,6 +8,7 @@ import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStreamExtractor;
 import org.schabi.newpipe.extractor.stream.StreamExtractor;
 import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
 import org.schabi.newpipe.extractor.stream.StreamType;
@@ -16,6 +17,7 @@ import java.io.IOException;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
+import java.util.TimeZone;
 
 import static java.util.Objects.requireNonNull;
 import static org.junit.Assert.*;
@@ -83,7 +85,9 @@ public class SoundcloudStreamExtractorDefaultTest {
         @Test
         public void testGetUploadDate() throws ParsingException, ParseException {
             final Calendar instance = Calendar.getInstance();
-            instance.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss +0000").parse("2016/07/31 18:18:07 +0000"));
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss +0000");
+            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+            instance.setTime(sdf.parse("2016/07/31 18:18:07 +0000"));
             assertEquals(instance, requireNonNull(extractor.getUploadDate()).date());
         }
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactoryTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactoryTest.java
index 5d303d10d..db2ae2b22 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactoryTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamLinkHandlerFactoryTest.java
@@ -5,6 +5,7 @@ import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudStreamLinkHandlerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractorTest.java
index 507f1ec2e..2bcc1d1ae 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudSubscriptionExtractorTest.java
@@ -7,6 +7,7 @@ import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.ServiceList;
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
+import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudSubscriptionExtractor;
 import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
 import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchExtractorTest.java
index 8503f5733..c99bf2a5b 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchExtractorTest.java
@@ -17,7 +17,7 @@ import java.net.URLEncoder;
 import static java.util.Collections.singletonList;
 import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
 import static org.schabi.newpipe.extractor.services.DefaultTests.assertNoDuplicatedItems;
-import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudSearchQueryHandlerFactory.*;
+import static org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudSearchQueryHandlerFactory.*;
 
 public class SoundcloudSearchExtractorTest {
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchQHTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchQHTest.java
index 58a7000d8..2efaa19dc 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchQHTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/search/SoundcloudSearchQHTest.java
@@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.NewPipe;
 import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
 import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
-import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudSearchQueryHandlerFactory.*;
+import static org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudSearchQueryHandlerFactory.*;
 
 public class SoundcloudSearchQHTest {
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeCommentsExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeCommentsExtractorTest.java
index 4349727be..a9f6dfa3b 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeCommentsExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeCommentsExtractorTest.java
@@ -1,6 +1,5 @@
 package org.schabi.newpipe.extractor.services.youtube;
 
-import org.jsoup.helper.StringUtil;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
@@ -11,11 +10,15 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 import org.schabi.newpipe.extractor.services.DefaultTests;
 import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsExtractor;
+import org.schabi.newpipe.extractor.utils.Utils;
 
 import java.io.IOException;
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.schabi.newpipe.extractor.ServiceList.YouTube;
 
 public class YoutubeCommentsExtractorTest {
@@ -68,15 +71,14 @@ public class YoutubeCommentsExtractorTest {
     private boolean getCommentsFromCommentsInfoHelper(String url) throws IOException, ExtractionException {
         boolean result = false;
         CommentsInfo commentsInfo = CommentsInfo.getInfo(url);
-        assertEquals("what the fuck am i doing with my life", commentsInfo.getName());
         result = findInComments(commentsInfo.getRelatedItems(), "s1ck m3m3");
 
-        String nextPage = commentsInfo.getNextPageUrl();
-        while (!StringUtil.isBlank(nextPage) && !result) {
+   /*     String nextPage = commentsInfo.getNextPageUrl();
+        while (!Utils.isBlank(nextPage) && !result) {
             InfoItemsPage moreItems = CommentsInfo.getMoreItems(YouTube, commentsInfo, nextPage);
             result = findInComments(moreItems.getItems(), "s1ck m3m3");
             nextPage = moreItems.getNextPageUrl();
-        }
+        }*/
         return result;
     }
 
@@ -86,16 +88,16 @@ public class YoutubeCommentsExtractorTest {
 
         DefaultTests.defaultTestListOfItems(YouTube, comments.getItems(), comments.getErrors());
         for (CommentsInfoItem c : comments.getItems()) {
-            assertFalse(StringUtil.isBlank(c.getAuthorEndpoint()));
-            assertFalse(StringUtil.isBlank(c.getAuthorName()));
-            assertFalse(StringUtil.isBlank(c.getAuthorThumbnail()));
-            assertFalse(StringUtil.isBlank(c.getCommentId()));
-            assertFalse(StringUtil.isBlank(c.getCommentText()));
-            assertFalse(StringUtil.isBlank(c.getName()));
-            assertFalse(StringUtil.isBlank(c.getTextualPublishedTime()));
-            assertNotNull(c.getPublishedTime());
-            assertFalse(StringUtil.isBlank(c.getThumbnailUrl()));
-            assertFalse(StringUtil.isBlank(c.getUrl()));
+            assertFalse(Utils.isBlank(c.getUploaderUrl()));
+            assertFalse(Utils.isBlank(c.getUploaderName()));
+            assertFalse(Utils.isBlank(c.getUploaderAvatarUrl()));
+            assertFalse(Utils.isBlank(c.getCommentId()));
+            assertFalse(Utils.isBlank(c.getCommentText()));
+            assertFalse(Utils.isBlank(c.getName()));
+            assertFalse(Utils.isBlank(c.getTextualUploadDate()));
+            assertNotNull(c.getUploadDate());
+            assertFalse(Utils.isBlank(c.getThumbnailUrl()));
+            assertFalse(Utils.isBlank(c.getUrl()));
             assertFalse(c.getLikeCount() < 0);
         }
     }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelperTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelperTest.java
index 669bc3292..0a3e007fc 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelperTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelperTest.java
@@ -5,7 +5,6 @@ import org.junit.Test;
 import org.schabi.newpipe.DownloaderTestImpl;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
-import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper;
 
 import java.io.IOException;
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubePlaylistExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubePlaylistExtractorTest.java
index 65bb7f998..f2e15a3fa 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubePlaylistExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubePlaylistExtractorTest.java
@@ -254,4 +254,102 @@ public class YoutubePlaylistExtractorTest {
             assertTrue("Error in the streams count", extractor.getStreamCount() > 100);
         }
     }
+
+    public static class LearningPlaylist implements BasePlaylistExtractorTest {
+        private static YoutubePlaylistExtractor extractor;
+
+        @BeforeClass
+        public static void setUp() throws Exception {
+            NewPipe.init(DownloaderTestImpl.getInstance());
+            extractor = (YoutubePlaylistExtractor) YouTube
+                    .getPlaylistExtractor("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8");
+            extractor.fetchPage();
+        }
+
+        /*//////////////////////////////////////////////////////////////////////////
+        // Extractor
+        //////////////////////////////////////////////////////////////////////////*/
+
+        @Test
+        public void testServiceId() {
+            assertEquals(YouTube.getServiceId(), extractor.getServiceId());
+        }
+
+        @Test
+        public void testName() throws Exception {
+            String name = extractor.getName();
+            assertTrue(name, name.startsWith("Anatomy & Physiology"));
+        }
+
+        @Test
+        public void testId() throws Exception {
+            assertEquals("PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getId());
+        }
+
+        @Test
+        public void testUrl() throws ParsingException {
+            assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getUrl());
+        }
+
+        @Test
+        public void testOriginalUrl() throws ParsingException {
+            assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getOriginalUrl());
+        }
+
+        /*//////////////////////////////////////////////////////////////////////////
+        // ListExtractor
+        //////////////////////////////////////////////////////////////////////////*/
+
+        @Test
+        public void testRelatedItems() throws Exception {
+            defaultTestRelatedItems(extractor);
+        }
+
+        @Ignore
+        @Test
+        public void testMoreRelatedItems() throws Exception {
+            defaultTestMoreItems(extractor);
+        }
+
+        /*//////////////////////////////////////////////////////////////////////////
+        // PlaylistExtractor
+        //////////////////////////////////////////////////////////////////////////*/
+
+        @Test
+        public void testThumbnailUrl() throws Exception {
+            final String thumbnailUrl = extractor.getThumbnailUrl();
+            assertIsSecureUrl(thumbnailUrl);
+            assertTrue(thumbnailUrl, thumbnailUrl.contains("yt"));
+        }
+
+        @Ignore
+        @Test
+        public void testBannerUrl() throws Exception {
+            final String bannerUrl = extractor.getBannerUrl();
+            assertIsSecureUrl(bannerUrl);
+            assertTrue(bannerUrl, bannerUrl.contains("yt"));
+        }
+
+        @Test
+        public void testUploaderUrl() throws Exception {
+            assertEquals("https://www.youtube.com/channel/UCX6b17PVsYBQ0ip5gyeme-Q", extractor.getUploaderUrl());
+        }
+
+        @Test
+        public void testUploaderName() throws Exception {
+            final String uploaderName = extractor.getUploaderName();
+            assertTrue(uploaderName, uploaderName.contains("CrashCourse"));
+        }
+
+        @Test
+        public void testUploaderAvatarUrl() throws Exception {
+            final String uploaderAvatarUrl = extractor.getUploaderAvatarUrl();
+            assertTrue(uploaderAvatarUrl, uploaderAvatarUrl.contains("yt"));
+        }
+
+        @Test
+        public void testStreamCount() throws Exception {
+            assertTrue("Error in the streams count", extractor.getStreamCount() > 40);
+        }
+    }
 }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamLinkHandlerFactoryTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamLinkHandlerFactoryTest.java
index 908642fdb..62398d597 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamLinkHandlerFactoryTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamLinkHandlerFactoryTest.java
@@ -79,6 +79,7 @@ public class YoutubeStreamLinkHandlerFactoryTest {
         assertEquals("EhxJLojIE_o", linkHandler.fromUrl("http://www.youtube.com/attribution_link?a=JdfC0C9V6ZI&u=%2Fwatch%3Fv%3DEhxJLojIE_o%26feature%3Dshare").getId());
         assertEquals("jZViOEv90dI", linkHandler.fromUrl("vnd.youtube://www.youtube.com/watch?v=jZViOEv90dI").getId());
         assertEquals("jZViOEv90dI", linkHandler.fromUrl("vnd.youtube:jZViOEv90dI").getId());
+        assertEquals("n8X9_MgEdCg", linkHandler.fromUrl("vnd.youtube://n8X9_MgEdCg").getId());
         assertEquals("O0EDx9WAelc", linkHandler.fromUrl("https://music.youtube.com/watch?v=O0EDx9WAelc").getId());
     }
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeMusicSearchExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeMusicSearchExtractorTest.java
index 420db0adb..eeac8c49b 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeMusicSearchExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeMusicSearchExtractorTest.java
@@ -150,4 +150,28 @@ public class YoutubeMusicSearchExtractorTest {
         @Nullable @Override public String expectedSearchSuggestion() { return "mega man x3"; }
         @Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
     }
+
+    public static class CorrectedSearch extends DefaultSearchExtractorTest {
+        private static SearchExtractor extractor;
+        private static final String QUERY = "duo lipa";
+        private static final String EXPECTED_SUGGESTION = "dua lipa";
+
+        @BeforeClass
+        public static void setUp() throws Exception {
+            NewPipe.init(DownloaderTestImpl.getInstance());
+            extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), "");
+            extractor.fetchPage();
+        }
+
+        @Override public SearchExtractor extractor() { return extractor; }
+        @Override public StreamingService expectedService() { return YouTube; }
+        @Override public String expectedName() { return QUERY; }
+        @Override public String expectedId() { return QUERY; }
+        @Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
+        @Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
+        @Override public String expectedSearchString() { return QUERY; }
+        @Nullable @Override public String expectedSearchSuggestion() { return EXPECTED_SUGGESTION; }
+        @Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
+        @Override public boolean isCorrectedSearch() { return true; }
+    }
 }
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchExtractorTest.java
index d5f2f1af2..518796fb2 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchExtractorTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchExtractorTest.java
@@ -20,6 +20,7 @@ import static org.schabi.newpipe.extractor.ExtractorAsserts.assertEmptyErrors;
 import static org.schabi.newpipe.extractor.ServiceList.YouTube;
 import static org.schabi.newpipe.extractor.services.DefaultTests.assertNoDuplicatedItems;
 import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.*;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
 
 public class YoutubeSearchExtractorTest {
     public static class All extends DefaultSearchExtractorTest {
@@ -113,6 +114,29 @@ public class YoutubeSearchExtractorTest {
     }
 
     public static class Suggestion extends DefaultSearchExtractorTest {
+        private static SearchExtractor extractor;
+        private static final String QUERY = "newpip";
+        private static final String EXPECTED_SUGGESTION = "newpipe";
+
+        @BeforeClass
+        public static void setUp() throws Exception {
+            NewPipe.init(DownloaderTestImpl.getInstance());
+            extractor = YouTube.getSearchExtractor(QUERY, singletonList(VIDEOS), "");
+            extractor.fetchPage();
+        }
+
+        @Override public SearchExtractor extractor() { return extractor; }
+        @Override public StreamingService expectedService() { return YouTube; }
+        @Override public String expectedName() { return QUERY; }
+        @Override public String expectedId() { return QUERY; }
+        @Override public String expectedUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
+        @Override public String expectedOriginalUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
+        @Override public String expectedSearchString() { return QUERY; }
+        @Nullable @Override public String expectedSearchSuggestion() { return EXPECTED_SUGGESTION; }
+        @Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
+    }
+
+    public static class CorrectedSearch extends DefaultSearchExtractorTest {
         private static SearchExtractor extractor;
         private static final String QUERY = "pewdeipie";
         private static final String EXPECTED_SUGGESTION = "pewdiepie";
@@ -132,8 +156,8 @@ public class YoutubeSearchExtractorTest {
         @Override public String expectedOriginalUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
         @Override public String expectedSearchString() { return QUERY; }
         @Nullable @Override public String expectedSearchSuggestion() { return EXPECTED_SUGGESTION; }
-
         @Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
+        @Override public boolean isCorrectedSearch() { return true; }
     }
 
     public static class RandomQueryNoMorePages extends DefaultSearchExtractorTest {
@@ -170,7 +194,7 @@ public class YoutubeSearchExtractorTest {
 
             assertFalse("More items available when it shouldn't", nextEmptyPage.hasNextPage());
             final String nextPageUrl = nextEmptyPage.getNextPageUrl();
-            assertTrue("Next page is not empty or null", nextPageUrl == null || nextPageUrl.isEmpty());
+            assertTrue("Next page is not empty or null", isNullOrEmpty(nextPageUrl));
         }
     }
 
diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java
index f5c7fe104..0e17fe8d8 100644
--- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java
+++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java
@@ -116,8 +116,7 @@ public class YoutubeStreamExtractorLivestreamTest {
 
     @Test
     public void testGetDashMpd() throws ParsingException {
-        // we dont expect this particular video to have a DASH file. For this purpouse we use a different test class.
-        assertTrue(extractor.getDashMpdUrl(), extractor.getDashMpdUrl().isEmpty());
+        assertTrue(extractor.getDashMpdUrl().startsWith("https://manifest.googlevideo.com/api/manifest/dash/"));
     }
 
     @Test
diff --git a/timeago-parser/build.gradle b/timeago-parser/build.gradle
index ff23db9ea..e324a2b4f 100644
--- a/timeago-parser/build.gradle
+++ b/timeago-parser/build.gradle
@@ -1,6 +1,6 @@
 dependencies {
-    testImplementation 'junit:junit:4.12'
+    testImplementation 'junit:junit:4.13'
 
     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
-    implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0'
+    implementation 'com.github.spotbugs:spotbugs-annotations:4.0.2'
 }